[同步更新]

This commit is contained in:
LA
2025-12-27 16:15:12 +08:00
parent 24e2f51918
commit ebd68b5c99
64 changed files with 6312 additions and 268 deletions

View File

@@ -300,7 +300,7 @@ namespace osu.Game.Rulesets.Catch
Description = "Affects how early fruits fade in on the screen.",
AdditionalMetrics =
[
new RulesetBeatmapAttribute.AdditionalMetric("Fade-in time", LocalisableString.Interpolate($@"{IBeatmapDifficultyInfo.DifficultyRange(effectiveDifficulty.ApproachRate, CatchHitObject.PREEMPT_RANGE):#,0.##} ms"))
new RulesetBeatmapAttribute.AdditionalMetric("Fade-in time", LocalisableString.Interpolate($@"{IBeatmapDifficultyInfo.DifficultyRangeInt(effectiveDifficulty.ApproachRate, CatchHitObject.PREEMPT_RANGE):#,0.##} ms"))
]
};
yield return new RulesetBeatmapAttribute(SongSelectStrings.HPDrain, @"HP", originalDifficulty.DrainRate, effectiveDifficulty.DrainRate, 10)

View File

@@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_RANGE);
TimePreempt = IBeatmapDifficultyInfo.DifficultyRangeInt(difficulty.ApproachRate, PREEMPT_RANGE);
Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize);
}

View File

@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Mods
typeof(ManiaModFadeIn)
}).ToArray();
public override bool Ranked => false;
public override bool Ranked => true;
public override bool ValidForFreestyleAsRequiredMod => false;

View File

@@ -27,6 +27,9 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase("multi-segment-slider")]
[TestCase("nan-slider")]
[TestCase("1124896")]
[TestCase("1341554")]
[TestCase("2593923")]
[TestCase("801165")]
public void Test(string name) => base.Test(name);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,941 @@
osu file format v14
[General]
AudioLeadIn: 0
PreviewTime: 76429
Countdown: 0
SampleSet: Soft
StackLeniency: 0.2
Mode: 0
LetterboxInBreaks: 0
WidescreenStoryboard: 1
[Difficulty]
HPDrainRate:5
CircleSize:4.3
OverallDifficulty:8
ApproachRate:9.3
SliderMultiplier:2.99999995231628
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]
763,444.444444444444,4,2,1,60,1,0
763,-111.111111111111,4,2,1,60,0,0
1929,-100,4,2,1,5,0,0
1985,-100,4,2,1,60,0,0
2040,-100,4,2,1,5,0,0
2096,-153.846153846153,4,2,1,60,0,0
2429,-133.333333333333,4,2,1,5,0,0
2540,-71.4285714285714,4,2,1,70,0,1
2985,-100,4,2,1,70,0,1
4485,-100,4,2,1,5,0,1
4540,-100,4,2,1,70,0,1
4707,-100,4,2,1,5,0,1
4762,-100,4,2,1,70,0,1
4929,-100,4,2,1,5,0,1
4985,-100,4,2,1,70,0,1
5096,-83.3333333333333,4,2,1,70,0,1
5429,-133.333333333333,4,2,1,70,0,1
5596,-133.333333333333,4,2,1,70,0,1
5652,-133.333333333333,4,2,1,70,0,1
5818,-133.333333333333,4,2,1,70,0,1
5874,-133.333333333333,4,2,1,70,0,1
6040,-133.333333333333,4,2,1,70,0,1
6096,-100,4,2,1,70,0,1
6540,-100,4,2,1,70,0,1
8040,-100,4,2,1,5,0,1
8096,-100,4,2,1,70,0,1
8262,-100,4,2,1,5,0,1
8318,-100,4,2,1,70,0,1
8485,-100,4,2,1,5,0,1
8540,-133.333333333333,4,2,1,70,0,1
8874,-100,4,2,1,5,0,1
8985,-100,4,2,1,70,0,1
9651,-100,4,2,1,70,0,1
10096,-100,4,2,1,70,0,1
11596,-100,4,2,1,5,0,1
11651,-100,4,2,1,70,0,1
11818,-100,4,2,1,5,0,1
11873,-100,4,2,1,70,0,1
11874,-80,4,2,1,70,0,1
12040,-80,4,2,1,5,0,1
12096,-80,4,2,1,70,0,1
12207,-133.333333333333,4,2,1,70,0,1
12429,-100,4,2,1,5,0,1
12540,-100,4,2,1,70,0,1
12707,-100,4,2,1,70,0,1
12763,-100,4,2,1,70,0,1
12929,-100,4,2,1,70,0,1
12985,-100,4,2,1,70,0,1
13429,-300,4,2,1,70,0,1
13651,-83.3333333333333,4,2,1,70,0,1
13874,-100,4,2,1,70,0,1
15151,-100,4,2,1,5,0,1
15207,-100,4,2,1,70,0,1
15373,-100,4,2,1,5,0,1
15429,-100,4,2,1,70,0,1
15596,-100,4,2,1,5,0,1
15651,-100,4,2,1,70,0,1
15985,-100,4,2,1,5,0,1
16096,-100,4,2,1,70,0,1
16262,-100,4,2,1,70,0,1
16318,-83.3333333333333,4,2,1,70,0,1
16651,-100,4,2,1,70,0,1
16762,-133.333333333333,4,2,1,60,0,0
17096,-133.333333333333,4,2,1,5,0,0
17207,-200,4,2,1,60,0,0
18096,-66.6666666666667,4,2,1,60,0,0
18262,-66.6666666666667,4,2,1,5,0,0
18318,-66.6666666666667,4,2,1,60,0,0
18540,-100,4,2,1,60,0,0
18874,-100,4,2,1,5,0,0
18985,-100,4,2,1,60,0,0
19985,-100,4,2,1,60,0,0
20485,-100,4,2,1,5,0,0
20540,-100,4,2,1,60,0,0
20707,-100,4,2,1,5,0,0
20762,-200,4,2,1,60,0,0
20985,-100,4,2,1,60,0,0
21095,-100,4,2,1,60,0,0
21374,-100,4,2,1,5,0,0
21429,-100,4,2,1,60,0,0
21596,-100,4,2,1,5,0,0
21651,-100,4,2,1,60,0,0
21818,-100,4,2,1,5,0,0
21874,-66.6666666666667,4,2,1,60,0,0
21985,-100,4,2,1,60,0,0
22096,-100,4,2,1,60,0,0
22985,-200,4,2,1,60,0,0
23318,-100,4,2,1,60,0,0
23429,-100,4,2,1,60,0,0
23540,-100,4,2,1,60,0,0
23651,-100,4,2,1,60,0,0
23762,-100,4,2,1,60,0,0
23874,-133.333333333333,4,2,1,60,0,0
24208,-133.333333333333,4,2,1,5,0,0
24318,-200,4,2,1,5,0,0
24319,-200,4,2,1,60,0,0
24540,-100,4,2,1,60,0,0
24651,-66.6666666666667,4,2,1,60,0,0
24874,-100,4,2,1,60,0,0
25374,-100,4,2,1,5,0,0
25429,-100,4,2,1,60,0,0
27096,-100,4,2,1,60,0,0
27596,-100,4,2,1,5,0,0
27651,-100,4,2,1,60,0,0
27818,-100,4,2,1,5,0,0
27873,-133.333333333333,4,2,1,60,0,0
28096,-100,4,2,1,60,0,0
28206,-100,4,2,1,60,0,0
28485,-100,4,2,1,5,0,0
28540,-100,4,2,1,60,0,0
28707,-100,4,2,1,5,0,0
28762,-100,4,2,1,60,0,0
28929,-100,4,2,1,5,0,0
28985,-66.6666666666667,4,2,1,60,0,0
29151,-100,4,2,1,60,0,0
29207,-100,4,2,1,60,0,0
29651,-100,4,2,1,60,0,0
30429,-100,4,2,1,60,0,0
30540,-58.8235294117647,4,2,1,60,0,0
30874,-58.8235294117647,4,2,1,5,0,0
30985,-58.8235294117647,4,2,1,60,0,0
31040,-100,4,2,1,5,0,0
31429,-100,4,2,1,60,0,0
32485,-100,4,2,1,60,0,0
32540,-100,4,2,1,60,0,0
32707,-100,4,2,1,60,0,0
32762,-100,4,2,1,60,0,0
32985,-100,4,2,1,60,0,0
34318,-50,4,2,1,60,0,0
34485,-100,4,2,1,5,0,0
34540,-100,4,2,1,60,0,0
35151,-100,4,2,1,5,0,0
35207,-100,4,2,1,60,0,0
35374,-100,4,2,1,5,0,0
35430,-100,4,2,1,60,0,0
35818,-100,4,2,1,5,0,0
35874,-200,4,2,1,60,0,0
36429,-100,4,2,1,60,0,0
37818,-100,4,2,1,5,0,0
37874,-100,4,2,1,60,0,0
38040,-100,4,2,1,5,0,0
38096,-50,4,2,1,60,0,0
38151,-100,4,2,1,5,0,0
38540,-100,4,2,1,60,0,0
39596,-100,4,2,1,5,0,0
39651,-100,4,2,1,60,0,0
39818,-100,4,2,1,60,0,0
39873,-100,4,2,1,60,0,0
40096,-100,4,2,1,60,0,0
41429,-50,4,2,1,60,0,0
41596,-100,4,2,1,5,0,0
41651,-100,4,2,1,60,0,0
41818,-100,4,2,1,5,0,0
41874,-100,4,2,1,60,0,0
42040,-100,4,2,1,5,0,0
42096,-100,4,2,1,60,0,0
44318,-100,4,2,1,60,0,0
44762,-83.3333333333333,4,2,1,60,0,0
45207,-66.6666666666667,4,2,1,45,0,0
45651,-133.333333333333,4,2,1,45,0,0
51540,-133.333333333333,4,2,1,50,0,0
51651,-133.333333333333,4,2,1,45,0,0
52318,-133.333333333333,4,2,1,45,0,0
58540,-76.9230769230769,4,2,1,45,0,0
58818,-100,4,2,1,45,0,0
58874,-111.111111111111,4,2,1,45,0,0
59318,-111.111111111111,4,2,1,45,0,0
59429,-83.3333333333333,4,2,1,60,0,0
59540,-83.3333333333333,4,2,1,5,0,0
59874,-100,4,2,1,60,0,0
60096,-100,4,2,1,5,0,0
60207,-100,4,2,1,60,0,0
60707,-100,4,2,1,5,0,0
60763,-100,4,2,1,60,0,0
60818,-100,4,2,1,5,0,0
60874,-100,4,2,1,60,0,0
60929,-100,4,2,1,5,0,0
60985,-100,4,2,1,60,0,0
61040,-100,4,2,1,5,0,0
61096,-100,4,2,1,60,0,0
61151,-100,4,2,1,5,0,0
61207,-100,4,2,1,60,0,0
61596,-100,4,2,1,5,0,0
61651,-100,4,2,1,60,0,0
61762,-83.3333333333333,4,2,1,60,0,0
61985,-100,4,2,1,5,0,0
62096,-100,4,2,1,60,0,0
62151,-100,4,2,1,5,0,0
62207,-100,4,2,1,60,0,0
62262,-100,4,2,1,5,0,0
62318,-100,4,2,1,60,0,0
62374,-100,4,2,1,5,0,0
62430,-100,4,2,1,60,0,0
62485,-100,4,2,1,5,0,0
62540,-100,4,2,1,60,0,0
62596,-100,4,2,1,5,0,0
62651,-100,4,2,1,60,0,0
62707,-100,4,2,1,5,0,0
62762,-100,4,2,1,60,0,0
62818,-100,4,2,1,5,0,0
62874,-100,4,2,1,60,0,0
62929,-100,4,2,1,60,0,0
62930,-100,4,2,1,5,0,0
62985,-100,4,2,1,60,0,0
63707,-100,4,2,1,5,0,0
63762,-100,4,2,1,60,0,0
64262,-100,4,2,1,5,0,0
64318,-100,4,2,1,60,0,0
64485,-100,4,2,1,5,0,0
64540,-100,4,2,1,60,0,0
64596,-100,4,2,1,5,0,0
64651,-100,4,2,1,60,0,0
64707,-100,4,2,1,5,0,0
64762,-71.4285714285714,4,2,1,60,0,0
64929,-71.4285714285714,4,2,1,5,0,0
64984,-133.333333333333,4,2,1,60,0,0
65151,-133.333333333333,4,2,1,5,0,0
65206,-71.4285714285714,4,2,1,60,0,0
65374,-71.4285714285714,4,2,1,5,0,0
65429,-133.333333333333,4,2,1,60,0,0
65596,-133.333333333333,4,2,1,5,0,0
65651,-100,4,2,1,60,0,0
66540,-66.6666666666667,4,2,1,60,0,0
66596,-66.6666666666667,4,2,1,5,0,0
66929,-100,4,2,1,5,0,0
66985,-200,4,2,1,60,0,0
67207,-200,4,2,1,5,0,0
67318,-100,4,2,1,60,0,0
67818,-100,4,2,1,5,0,0
67874,-100,4,2,1,60,0,0
67929,-100,4,2,1,5,0,0
67985,-100,4,2,1,60,0,0
68040,-100,4,2,1,5,0,0
68096,-100,4,2,1,60,0,0
68151,-100,4,2,1,5,0,0
68207,-100,4,2,1,60,0,0
68262,-100,4,2,1,5,0,0
68318,-100,4,2,1,60,0,0
68874,-83.3333333333333,4,2,1,60,0,0
69096,-100,4,2,1,60,0,0
69097,-100,4,2,1,5,0,0
69207,-100,4,2,1,60,0,0
69263,-100,4,2,1,5,0,0
69319,-100,4,2,1,60,0,0
69374,-100,4,2,1,5,0,0
69430,-100,4,2,1,60,0,0
69486,-100,4,2,1,5,0,0
69542,-100,4,2,1,60,0,0
69597,-100,4,2,1,5,0,0
69651,-100,4,2,1,60,0,0
69707,-100,4,2,1,5,0,0
69762,-100,4,2,1,60,0,0
69818,-100,4,2,1,5,0,0
69874,-100,4,2,1,60,0,0
69929,-100,4,2,1,5,0,0
69985,-100,4,2,1,60,0,0
70040,-100,4,2,1,60,0,0
70041,-100,4,2,1,5,0,0
70096,-100,4,2,1,60,0,0
70818,-100,4,2,1,5,0,0
70873,-100,4,2,1,60,0,0
71207,-71.4285714285714,4,2,1,60,0,0
71429,-100,4,2,1,60,0,0
71874,-71.4285714285714,4,2,1,60,0,0
72041,-71.4285714285714,4,2,1,5,0,0
72096,-133.333333333333,4,2,1,60,0,0
72263,-133.333333333333,4,2,1,5,0,0
72318,-71.4285714285714,4,2,1,60,0,0
72485,-71.4285714285714,4,2,1,5,0,0
72540,-133.333333333333,4,2,1,60,0,0
72985,-66.6666666666667,4,2,1,60,0,0
73207,-100,4,2,1,60,0,0
73651,-133.333333333333,4,2,1,45,0,0
75318,-133.333333333333,4,2,1,5,0,0
75429,-133.333333333333,4,2,1,45,0,0
76762,-100,4,2,1,45,0,0
77096,-100,4,2,1,5,0,0
77207,-100,4,2,1,70,0,1
77818,-100,4,2,1,5,0,1
77874,-100,4,2,1,70,0,1
78262,-100,4,2,1,5,0,1
78318,-100,4,2,1,70,0,1
78540,-83.3333333333333,4,2,1,70,0,1
78985,-100,4,2,1,70,0,1
79596,-100,4,2,1,5,0,1
79651,-100,4,2,1,70,0,1
80040,-100,4,2,1,5,0,1
80096,-100,4,2,1,70,0,1
80318,-83.3333333333333,4,2,1,70,0,1
84318,-100,4,2,1,70,0,1
84929,-100,4,2,1,5,0,1
84985,-100,4,2,1,70,0,1
85207,-100,4,2,1,70,0,1
85374,-100,4,2,1,5,0,1
85429,-100,4,2,1,70,0,1
85651,-83.3333333333333,4,2,1,70,0,1
86096,-100,4,2,1,70,0,1
86707,-100,4,2,1,5,0,1
86762,-100,4,2,1,70,0,1
88818,-100,4,2,1,5,0,1
88874,-100,4,2,1,70,0,1
88929,-100,4,2,1,5,0,1
88985,-100,4,2,1,70,0,1
89040,-100,4,2,1,5,0,1
89096,-100,4,2,1,70,0,1
92040,-100,4,2,1,5,0,1
92096,-100,4,2,1,70,0,1
92485,-100,4,2,1,5,0,1
92540,-100,4,2,1,70,0,1
97651,-200,4,2,1,70,0,1
97818,-200,4,2,1,5,0,1
97874,-66.6666666666667,4,2,1,70,0,1
97985,-66.6666666666667,4,2,1,70,0,1
98040,-66.6666666666667,4,2,1,5,0,1
98096,-133.333333333333,4,2,1,70,0,1
98262,-133.333333333333,4,2,1,5,0,1
98318,-66.6666666666667,4,2,1,70,0,1
98540,-100,4,2,1,70,0,1
99151,-100,4,2,1,5,0,1
99207,-100,4,2,1,70,0,1
99596,-100,4,2,1,5,0,1
99651,-100,4,2,1,70,0,1
103040,-100,4,2,1,5,0,1
103096,-100,4,2,1,70,0,1
103151,-100,4,2,1,5,0,1
103207,-100,4,2,1,70,0,1
103262,-100,4,2,1,5,0,1
103318,-100,4,2,1,70,0,1
105207,-83.3333333333333,4,2,1,70,0,1
105540,-83.3333333333333,4,2,1,70,0,1
105651,-133.333333333333,4,2,1,60,0,0
105985,-133.333333333333,4,2,1,5,0,0
106096,-200,4,2,1,60,0,0
106985,-66.6666666666667,4,2,1,60,0,0
107151,-66.6666666666667,4,2,1,5,0,0
107207,-66.6666666666667,4,2,1,60,0,0
107429,-100,4,2,1,60,0,0
107763,-100,4,2,1,5,0,0
107874,-100,4,2,1,60,0,0
108874,-100,4,2,1,60,0,0
109374,-100,4,2,1,5,0,0
109429,-100,4,2,1,60,0,0
109596,-100,4,2,1,5,0,0
109651,-200,4,2,1,60,0,0
109929,-100,4,2,1,60,0,0
109984,-100,4,2,1,60,0,0
110262,-100,4,2,1,5,0,0
110318,-100,4,2,1,60,0,0
110485,-100,4,2,1,5,0,0
110540,-100,4,2,1,60,0,0
110707,-100,4,2,1,5,0,0
110762,-66.6666666666667,4,2,1,60,0,0
110929,-100,4,2,1,60,0,0
110985,-133.333333333333,4,2,1,60,0,0
111429,-133.333333333333,4,2,1,60,0,0
111596,-133.333333333333,4,2,1,60,0,0
111651,-133.333333333333,4,2,1,60,0,0
111818,-133.333333333333,4,2,1,60,0,0
111874,-100,4,2,1,60,0,1
112318,-83.3333333333333,4,2,1,60,0,1
112429,-100,4,2,1,5,0,0
[Colours]
Combo1 : 112,75,180
Combo2 : 0,255,255
Combo3 : 255,15,117
Combo4 : 255,135,15
[HitObjects]
309,230,763,37,0,3:0:0:0:
485,146,985,2,0,L|406:167,1,67.4999968671799,8|0,3:0|0:0,0:0:0:0:
374,249,1207,2,0,L|299:227,1,67.4999968671799,8|0,3:0|0:0,0:0:0:0:
196,91,1429,2,0,L|191:44,3,33.7499984335899,0|0|0|0,3:0|3:0|3:0|3:0,0:0:0:0:
124,173,1651,2,0,L|131:222,2,44.9999979114532,0|0|0,3:0|3:0|3:0,0:0:0:0:
221,284,1874,2,0,L|213:208,1,67.4999968671799,0|0,3:0|3:0,0:0:0:0:
292,86,2096,38,0,L|310:234,1,146.249990980625,12|0,3:0|0:0,0:0:0:0:
314,328,2540,38,0,B|280:359|280:359|230:320|252:242|313:230,1,209.999990253448,0|0,3:0|0:0,0:0:0:0:
421,300,2874,1,0,0:0:0:0:
421,300,2985,2,0,P|461:288|491:253,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
309,231,3207,2,0,P|297:190|305:153,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
394,22,3429,5,0,3:0:0:0:
461,72,3540,2,0,B|477:103|477:103|461:148,1,74.999998807907,0|4,0:0|0:0,0:0:0:0:
378,183,3762,2,0,L|206:157,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
229,161,4096,2,0,P|227:202|211:250,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
61,384,4318,38,0,P|101:359|134:322,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
317,310,4540,2,0,P|267:305|226:288,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
141,110,4762,2,0,B|121:175|152:226|152:226|152:202|161:183,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
155,196,5096,6,0,P|67:211|79:286,1,179.999991645813,0|0,0:0|0:0,0:0:0:0:
212,366,5429,38,0,P|207:335|174:281,1,56.2500012516975,4|0,0:0|0:0,0:0:0:0:
206,286,5651,2,0,P|236:297|299:295,1,56.2500012516975,8|0,3:0|0:0,0:0:0:0:
281,321,5874,2,0,P|257:340|227:396,1,56.2500012516975,4|0,0:0|0:0,0:0:0:0:
124,246,6096,6,0,P|198:198|277:232,1,149.999997615814,0|0,3:0|0:0,0:0:0:0:
253,211,6429,1,0,0:0:0:0:
276,99,6540,2,0,P|335:139|369:215,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
368,208,6874,1,0,0:0:0:0:
430,96,6985,37,0,3:0:0:0:
497,147,7096,2,0,P|507:189|488:244,1,74.999998807907,0|4,0:0|0:0,0:0:0:0:
414,379,7318,2,0,B|383:322|421:267|421:267|421:308,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
421,298,7651,2,0,P|378:312|336:304,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
270,170,7874,6,0,P|275:228|236:278,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
94,300,8096,2,0,P|133:263|208:274,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
261,374,8318,2,0,L|176:365,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
38,377,8540,2,0,L|55:197,1,168.750003755093,4|0,0:0|0:0,0:0:0:0:
123,25,8985,38,0,L|132:110,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
217,242,9207,2,0,L|237:168,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
48,92,9429,5,4,0:0:0:0:
63,176,9540,1,0,0:0:0:0:
83,259,9651,38,0,P|167:223|231:255,1,149.999997615814,0|0,3:0|0:0,0:0:0:0:
274,312,9985,1,0,0:0:0:0:
274,312,10096,2,0,L|354:292,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
459,225,10318,2,0,L|375:204,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
269,107,10540,1,0,3:0:0:0:
276,54,10651,1,0,0:0:0:0:
313,17,10762,1,4,0:0:0:0:
363,9,10874,1,0,0:0:0:0:
363,9,11096,5,0,0:0:0:0:
432,68,11207,2,0,P|444:107|425:154,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
309,252,11429,38,0,P|297:195|321:158,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
450,316,11651,2,0,L|361:312,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
160,341,11874,2,0,B|187:380|187:380|233:309|177:235,1,187.499997019767,8|4,3:0|0:0,0:0:0:0:
116,200,12207,6,0,P|52:224|122:264,1,168.750003755093,0|4,0:0|0:0,0:0:0:0:
297,91,12762,37,8,3:0:0:0:
276,44,12874,1,0,0:0:0:0:
226,27,12985,1,4,0:0:0:0:
187,63,13096,1,0,0:0:0:0:
196,115,13207,1,0,0:0:0:0:
376,144,13429,2,0,L|378:121,2,16.6666664017571,0|0|0,0:0|0:0|0:0,0:0:0:0:
436,220,13651,6,0,B|395:211|373:164|373:164|332:208|264:185,1,179.999991645813,8|4,3:0|0:0,0:0:0:0:
276,44,13985,1,0,0:0:0:0:
196,115,14096,38,0,L|139:124,4,37.4999994039535,0|0|0|0|4,3:0|0:0|0:0|0:0|0:0,0:0:0:0:
82,69,14429,1,0,0:0:0:0:
106,190,14540,2,0,L|126:276,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
218,383,14762,2,0,L|234:309,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
26,231,14985,5,0,3:0:0:0:
253,202,15207,37,0,0:0:0:0:
331,271,15318,1,0,0:0:0:0:
233,309,15429,1,8,3:0:0:0:
389,73,15651,6,0,P|410:22|447:112,1,224.999996423721,4|0,3:0|0:0,0:0:0:0:
391,165,16096,1,0,0:0:0:0:
377,177,16207,1,0,0:0:0:0:
365,187,16318,38,0,B|253:261|221:119|94:192,1,269.999987468719,0|0,0:0|0:0,0:0:0:0:
73,319,16762,22,0,P|133:336|116:236,1,168.750003755093,4|0,3:0|0:0,0:0:0:0:
139,258,17207,6,0,P|138:315|69:283,1,112.500002503395,8|0,3:0|0:0,0:0:0:0:
92,323,17762,37,0,0:0:0:0:
43,245,17874,1,4,0:0:0:0:
4,322,17985,1,0,0:0:0:0:
133,245,18096,1,0,3:0:0:0:
29,105,18318,6,0,L|38:40,3,56.2500012516975,4|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
50,30,18540,38,0,P|111:56|193:25,1,149.999997615814,0|0,3:0|0:0,0:0:0:0:
240,120,18985,2,0,P|328:91|394:125,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
409,213,19318,2,0,B|377:226|377:226|243:200,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
119,187,19651,2,0,L|127:286,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
179,338,19874,1,8,3:0:0:0:
45,307,19985,6,0,L|3:297,2,37.4999994039535,0|0|4,0:0|0:0|0:0,0:0:0:0:
103,380,20207,1,0,3:0:0:0:
212,257,20318,38,0,P|233:218|231:171,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
111,118,20540,1,4,0:0:0:0:
111,118,20762,6,0,L|197:109,1,74.999998807907,8|4,3:0|0:0,0:0:0:0:
256,18,21096,37,0,0:0:0:0:
337,121,21207,2,0,P|350:60|403:16,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
384,26,21429,2,0,P|406:86|465:122,1,112.49999821186,0|0,0:0|0:0,0:0:0:0:
443,114,21651,2,0,P|377:105|327:131,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
352,223,21874,6,0,B|369:230|369:230|391:228|391:228|416:239|416:239|440:235|440:235|462:244|462:244|489:249,1,112.500002503395,4|0,0:0|0:0,0:0:0:0:
322,343,22096,37,0,3:0:0:0:
259,270,22207,2,0,P|223:276|182:263,2,74.999998807907,0|4|0,0:0|0:0|0:0,0:0:0:0:
86,360,22540,5,8,3:0:0:0:
15,295,22651,2,0,L|0:201,2,74.999998807907,0|4|0,0:0|0:0|0:0,0:0:0:0:
94,384,22985,38,0,P|118:328|112:277,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
0,211,23429,22,0,L|76:196,1,74.999998807907,12|0,3:0|0:0,0:0:0:0:
215,134,23651,2,0,L|114:110,1,74.999998807907,12|0,3:0|0:0,0:0:0:0:
33,124,23874,22,0,L|43:2,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
150,269,24318,2,0,L|162:194,1,74.999998807907,0|4,3:0|0:0,0:0:0:0:
229,134,24651,6,0,L|386:164,1,112.500002503395,12|0,3:0|0:0,0:0:0:0:
486,268,24874,37,0,0:0:0:0:
410,119,24985,1,4,0:0:0:0:
381,213,25096,1,0,0:0:0:0:
512,120,25207,1,0,3:0:0:0:
247,36,25429,6,0,L|191:25,3,37.4999994039535,4|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
185,24,25651,2,0,B|145:72|145:72|174:164,1,149.999997615814,0|0,3:0|0:0,0:0:0:0:
253,219,26096,2,0,B|281:311|281:311|228:382,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
100,363,26429,38,0,L|259:354,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
404,262,26762,1,4,0:0:0:0:
390,352,26874,1,0,0:0:0:0:
314,295,26985,1,8,3:0:0:0:
425,256,27096,6,0,L|492:246,2,37.4999994039535,0|0|4,0:0|0:0|0:0,0:0:0:0:
329,216,27318,1,0,3:0:0:0:
193,177,27429,38,0,L|266:161,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
322,107,27651,1,4,0:0:0:0:
322,107,27874,2,0,L|310:238,1,112.500002503395,8|4,3:0|0:0,0:0:0:0:
110,299,28207,5,0,0:0:0:0:
164,231,28318,2,0,B|168:303|168:303|121:338,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
30,284,28540,2,0,B|90:244|90:244|144:267,1,112.49999821186,0|0,0:0|0:0,0:0:0:0:
148,371,28762,2,0,B|83:338|83:338|76:280,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
194,201,28985,38,0,B|207:210|207:210|227:210|227:210|243:217|243:217|265:218|265:218|282:227|282:227|305:225|305:225|325:238,1,112.500002503395,4|0,0:0|0:0,0:0:0:0:
492,114,29207,6,0,P|445:136|410:138,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
324,102,29429,2,0,P|291:68|280:29,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
418,17,29651,1,8,3:0:0:0:
495,201,29874,1,4,3:2:0:0:
221,136,30096,37,0,3:0:0:0:
299,188,30207,2,0,B|316:251|316:251|271:352,1,149.999997615814,4|0,3:0|0:0,0:0:0:0:
115,334,30540,6,0,P|11:252|167:266,1,382.500001215934,0|0,0:0|0:0,0:0:0:0:
216,326,30985,38,0,L|304:331,1,63.7500002026557,4|0,3:0|0:0,0:0:0:0:
280,330,31429,6,0,L|293:241,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
426,252,31651,2,0,L|439:163,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
253,158,31874,37,0,3:0:0:0:
258,132,31985,1,0,0:0:0:0:
337,111,32096,5,4,0:0:0:0:
341,85,32207,1,0,0:0:0:0:
271,30,32318,38,0,B|212:42|212:42|141:19,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
163,26,32540,2,0,L|144:181,1,149.999997615814,4|0,0:0|3:0,0:0:0:0:
445,343,32985,22,0,B|439:234|439:234|384:269,1,149.999997615814,4|8,0:0|3:0,0:0:0:0:
240,257,33429,2,0,B|263:148|263:148|291:205,1,149.999997615814,4|0,0:0|3:0,0:0:0:0:
68,333,33874,2,0,B|83:233|83:233|41:256,1,149.999997615814,4|8,0:0|3:0,0:0:0:0:
344,347,34318,22,0,B|368:372|368:372|455:355|455:355|472:308,1,149.999997615814,4|0,0:0|0:0,0:0:0:0:
452,255,34540,2,0,B|389:212|389:212|332:273,1,149.999997615814,0|4,3:0|0:0,0:0:0:0:
256,220,34874,5,0,0:0:0:0:
256,220,34985,2,0,B|256:128,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
256,70,35207,2,0,B|256:162,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
112,312,35429,37,0,3:0:0:0:
60,255,35540,2,0,B|123:212|123:212|180:273,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
169,350,35874,6,0,B|144:375|144:375|57:358|57:358|40:311,1,149.999997615814,8|0,3:0|3:0,0:0:0:0:
62,169,36429,6,0,L|76:267,2,74.999998807907,0|4|0,0:0|0:0|0:0,0:0:0:0:
134,61,36762,1,8,3:0:0:0:
201,113,36874,2,0,L|215:211,2,74.999998807907,0|4|0,0:0|0:0|0:0,0:0:0:0:
298,272,37207,6,0,L|315:184,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
330,114,37429,1,4,0:0:0:0:
446,176,37540,2,0,B|404:214|404:214|307:197,1,149.999997615814,4|0,3:0|0:0,0:0:0:0:
231,240,37874,2,0,P|223:199|231:162,1,74.999998807907,4|0,3:0|0:0,0:0:0:0:
325,285,38096,6,0,L|154:300,1,149.999997615814,4|0,3:0|0:0,0:0:0:0:
175,298,38540,6,0,L|163:396,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
75,208,38762,2,0,L|63:306,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
233,74,38985,37,0,3:0:0:0:
231,98,39096,1,0,0:0:0:0:
156,139,39207,5,4,0:0:0:0:
155,165,39318,1,0,0:0:0:0:
227,215,39429,38,0,P|282:209|352:230,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
336,222,39651,2,0,L|366:67,1,149.999997615814,4|0,0:0|3:0,0:0:0:0:
81,35,40096,22,0,B|82:105|82:105|118:136|118:136|132:89,1,149.999997615814,4|8,0:0|3:0,0:0:0:0:
272,158,40540,2,0,B|270:228|270:228|234:259|234:259|220:212,1,149.999997615814,4|0,0:0|3:0,0:0:0:0:
423,36,40985,2,0,B|400:102|400:102|423:143|423:143|453:104,1,149.999997615814,4|8,0:0|3:0,0:0:0:0:
512,278,41429,6,0,P|415:258|361:293,1,149.999997615814,4|0,0:0|0:0,0:0:0:0:
359,302,41651,6,0,B|320:264|320:264|310:187,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
322,190,41874,2,0,L|449:171,1,112.49999821186,4|0,0:0|0:0,0:0:0:0:
443,159,42096,1,8,3:0:0:0:
240,52,42318,6,0,B|255:79|255:79|241:135,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
177,166,42540,1,0,3:0:0:0:
163,151,42651,2,0,B|161:207|161:207|192:240|192:240|189:299,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
131,365,42985,2,0,P|198:322|280:345,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
335,377,43318,1,0,0:0:0:0:
442,239,43429,38,0,B|456:178|456:178|422:136|422:136|427:68|427:68|449:112,1,224.999996423721,0|0,3:0|0:0,0:0:0:0:
444,103,43874,2,0,P|402:118|356:120,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
249,28,44096,2,0,P|295:35|324:48,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
364,201,44318,5,0,0:0:0:0:
332,195,44429,1,0,0:0:0:0:
251,135,44540,37,0,0:0:0:0:
281,123,44651,1,0,0:0:0:0:
332,195,44762,6,0,B|356:269|324:333|324:333|303:293,1,179.999991645813,4|0,0:3|0:0,0:0:0:0:
61,25,45207,38,0,L|88:158,1,112.500002503395,0|0,3:0|0:0,0:0:0:0:
84,136,45651,1,8,3:0:0:0:
84,136,46096,1,0,3:0:0:0:
176,33,46207,2,0,L|164:103,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
219,207,46429,2,0,L|232:152,1,56.2500012516975,0|8,0:0|3:0,0:0:0:0:
312,65,46651,1,0,0:0:0:0:
312,65,46762,2,0,L|398:94,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
512,176,46985,5,0,3:0:0:0:
421,192,47096,1,0,0:0:0:0:
421,192,47429,1,8,3:0:0:0:
402,357,47651,37,0,0:0:0:0:
394,277,47762,1,0,0:0:0:0:
328,324,47874,1,0,3:0:0:0:
328,324,48318,1,8,3:0:0:0:
110,357,48540,5,0,0:0:0:0:
118,277,48651,1,0,0:0:0:0:
184,324,48763,1,0,3:0:0:0:
110,357,48874,1,0,0:0:0:0:
110,357,49207,1,8,3:0:0:0:
110,357,49651,1,0,3:0:0:0:
0,283,49762,38,0,P|41:301|97:295,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
188,219,49985,2,0,P|168:236|137:246,1,56.2500012516975,0|8,0:0|3:0,0:0:0:0:
49,137,50207,1,0,0:0:0:0:
49,137,50318,2,0,P|65:184|93:205,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
107,67,50540,5,0,3:0:0:0:
32,15,50651,1,0,0:0:0:0:
32,15,50985,1,8,3:0:0:0:
265,114,51207,37,0,0:0:0:0:
254,196,51318,1,0,0:0:0:0:
241,279,51429,1,0,3:0:0:0:
241,279,51651,1,0,0:0:0:0:
336,207,51762,6,0,P|397:191|371:274,1,168.750003755093,0|0,0:0|0:0,0:0:0:0:
83,206,52318,5,0,3:0:0:0:
83,206,52429,2,0,L|101:260,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
40,383,52651,2,0,P|70:355|90:324,1,56.2500012516975,0|8,0:0|3:0,0:0:0:0:
214,334,52874,1,0,0:0:0:0:
214,334,52985,2,0,P|171:322|140:304,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
151,160,53207,5,0,3:0:0:0:
188,135,53318,1,0,0:0:0:0:
232,129,53429,1,0,0:0:0:0:
273,146,53540,1,0,0:0:0:0:
339,198,53651,37,8,3:0:0:0:
383,199,53762,1,0,0:0:0:0:
426,185,53874,1,0,0:0:0:0:
450,147,53985,1,0,0:0:0:0:
444,61,54096,6,0,P|414:28|377:15,1,56.2500012516975,0|0,3:0|0:0,0:0:0:0:
301,28,54318,2,0,P|268:48|255:77,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
189,271,54540,38,0,P|209:222|204:198,1,56.2500012516975,8|0,3:0|0:0,0:0:0:0:
186,114,54762,2,0,P|152:74|124:68,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
27,137,54985,5,0,3:0:0:0:
34,167,55096,1,0,0:0:0:0:
122,204,55207,37,0,0:0:0:0:
116,178,55318,1,0,0:0:0:0:
48,249,55429,5,8,3:0:0:0:
54,274,55540,1,0,0:0:0:0:
124,329,55651,38,0,P|157:326|200:310,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
320,185,55874,5,0,3:0:0:0:
287,175,55985,1,0,0:0:0:0:
254,181,56096,2,0,P|258:221|264:241,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
337,347,56318,2,0,P|348:321|350:293,1,56.2500012516975,8|0,3:0|0:0,0:0:0:0:
418,197,56540,37,0,0:0:0:0:
418,197,56651,2,0,L|492:180,1,56.2500012516975,0|0,0:0|3:0,0:0:0:0:
329,114,56874,2,0,L|262:94,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
436,59,57096,6,0,L|413:126,1,56.2500012516975,0|8,0:0|3:0,0:0:0:0:
332,194,57318,2,0,L|353:259,2,56.2500012516975,0|0|0,0:0|0:0|0:0,0:0:0:0:
202,194,57651,37,0,3:0:0:0:
224,233,57762,1,0,0:0:0:0:
222,279,57874,1,0,0:0:0:0:
193,314,57985,1,0,0:0:0:0:
144,244,58096,5,0,0:0:0:0:
127,214,58207,1,0,0:0:0:0:
126,180,58318,1,0,0:0:0:0:
139,149,58429,1,0,0:0:0:0:
224,113,58540,38,0,B|262:88|235:70|189:83|189:83|224:138|194:193,1,194.999987974167
299,319,58874,1,0,0:0:0:0:
299,319,58985,2,0,B|316:283|314:237|314:237|278:226|278:226|320:243|359:227,1,202.49999060154,4|0,0:0|0:0,0:0:0:0:
428,181,59429,22,0,P|454:129|399:4,1,179.999991645813,0|0,3:0|0:0,0:0:0:0:
418,18,59874,2,0,L|373:15,6,24.9999996026357,8|0|0|0|0|0|4,3:0|0:0|0:0|0:0|0:0|0:0|0:0,0:0:0:0:
428,181,60207,5,0,0:0:0:0:
352,209,60318,1,0,3:0:0:0:
278,177,60429,1,0,0:0:0:0:
208,225,60540,2,0,L|222:267,2,37.4999994039535,4|0|0,0:0|0:0|0:0,0:0:0:0:
71,144,60762,38,0,L|65:109,3,24.9999996026357,8|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
145,86,60985,1,4,0:0:0:0:
163,127,61096,1,0,0:0:0:0:
161,171,61207,1,0,3:0:0:0:
136,208,61318,1,0,0:0:0:0:
99,231,61429,2,0,B|91:279|91:279|117:314,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
177,378,61651,5,8,3:0:0:0:
177,378,61762,2,0,B|231:371|231:371|272:326|272:326|345:319,1,179.999991645813
417,293,62096,1,0,3:0:0:0:
438,263,62207,1,0,0:0:0:0:
436,225,62318,1,4,0:0:0:0:
412,196,62429,1,0,0:0:0:0:
320,172,62540,6,0,P|307:192|291:204,1,37.4999994039535,8|0,3:0|0:0,0:0:0:0:
291,147,62651,2,0,P|274:156|245:153,1,37.4999994039535,0|0,0:0|0:0,0:0:0:0:
276,114,62762,2,0,P|250:107|234:94,1,37.4999994039535,4|0,0:0|0:0,0:0:0:0:
283,81,62874,2,0,P|265:61|260:45,1,37.4999994039535,0|0,0:0|0:0,0:0:0:0:
365,31,62985,38,0,P|398:44|442:49,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
512,169,63207,2,0,P|466:163|421:176,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
350,107,63429,1,8,3:0:0:0:
293,237,63540,38,0,L|276:158,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
428,269,63762,2,0,B|373:275|373:275|338:249|338:249|267:255,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
191,318,64096,2,0,B|182:355|182:355|212:395,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
192,186,64318,5,8,3:0:0:0:
135,253,64429,2,0,L|56:270,2,74.999998807907,0|4|0,0:0|0:0|0:0,0:0:0:0:
24,136,64762,38,0,P|69:76|158:75,1,157.499992690086,0|0,3:0|0:0,0:0:0:0:
160,80,64985,6,0,P|193:102|255:102,1,84.3750018775463,4|0,0:0|0:0,0:0:0:0:
276,34,65207,38,0,L|290:212,1,157.499992690086,8|0,3:0|0:0,0:0:0:0:
291,219,65429,6,0,L|311:132,1,84.3750018775463,4|0,0:0|0:0,0:0:0:0:
381,111,65651,38,0,B|418:126|418:126|460:126,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
221,163,65874,2,0,B|186:143|186:143|139:153,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
41,231,66096,1,8,3:0:0:0:
49,267,66207,1,0,0:0:0:0:
56,303,66318,1,4,0:0:0:0:
67,288,66429,1,4,0:0:0:0:
77,270,66540,6,0,P|171:255|72:350,1,337.500007510185,0|0,0:0|0:0,0:0:0:0:
95,356,66985,38,0,L|185:343,1,74.999998807907,8|4,3:0|0:0,0:0:0:0:
274,286,67318,6,0,B|289:324|289:324|268:378,1,74.999998807907,0|0,0:0|3:0,0:0:0:0:
191,227,67540,1,0,0:0:0:0:
255,168,67651,2,0,L|264:116,2,37.4999994039535,4|0|0,0:0|0:0|0:0,0:0:0:0:
147,83,67874,2,0,L|154:108,3,24.9999996026357,8|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
80,148,68096,38,0,L|98:224,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
125,356,68318,1,0,3:0:0:0:
0,319,68429,1,0,0:0:0:0:
0,319,68540,2,0,L|76:294,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
277,219,68762,5,8,3:0:0:0:
277,219,68874,2,0,B|327:199|327:199|293:138|197:173,1,179.999991645813,0|0,0:0|0:0,0:0:0:0:
157,273,69207,37,0,3:0:0:0:
175,316,69318,1,0,0:0:0:0:
212,334,69429,1,4,0:0:0:0:
254,333,69540,1,0,0:0:0:0:
332,268,69651,38,0,P|333:237|343:213,1,37.4999994039535,8|0,3:0|0:0,0:0:0:0:
373,265,69762,2,0,P|386:239|404:232,1,37.4999994039535,0|0,0:0|0:0,0:0:0:0:
413,284,69874,2,0,P|430:269|454:269,1,37.4999994039535,4|0,0:0|0:0,0:0:0:0:
433,318,69985,2,0,P|452:320|474:337,1,37.4999994039535,4|0,0:0|0:0,0:0:0:0:
401,384,70096,6,0,P|353:378|319:346,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
251,251,70318,2,0,P|240:196|260:154,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
401,18,70540,1,8,3:0:0:0:
401,18,70651,2,0,P|409:54|398:90,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
327,193,70874,2,0,L|304:45,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
290,26,71207,6,0,L|308:144,1,104.999995126724,0|0,0:0|0:0,0:0:0:0:
272,302,71429,2,0,L|187:288,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
33,217,71651,37,4,0:0:0:0:
27,187,71762,1,4,0:0:0:0:
20,157,71874,2,0,B|103:140|103:140|162:58,1,157.499992690086,0|0,3:0|0:0,0:0:0:0:
145,82,72096,6,0,L|218:75,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
336,136,72318,38,0,P|331:213|231:208,1,157.499992690086
263,232,72540,6,0,L|278:300,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
183,384,72762,2,0,L|172:307,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
37,140,72985,38,0,B|10:168|10:168|17:204|17:204|54:220|54:220|89:196|89:196|87:157|87:157|57:138,1,225.00000500679
275,372,73651,6,0,P|320:352|387:369,1,112.500002503395
380,364,74096,2,0,L|436:358,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
495,271,74318,2,0,L|424:282,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
339,270,74540,1,0,0:0:0:0:
339,270,74651,1,0,0:0:0:0:
339,270,74762,2,0,L|329:196,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
408,46,74985,38,0,L|392:120,1,56.2500012516975
220,230,75207,2,0,L|209:156,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
282,7,75429,37,0,0:0:0:0:
300,98,75540,1,0,0:0:0:0:
197,25,75651,5,0,0:0:0:0:
222,103,75762,1,0,0:0:0:0:
126,69,75874,5,0,0:0:0:0:
153,134,75985,1,0,0:0:0:0:
76,145,76096,5,0,0:0:0:0:
116,179,76207,1,0,0:0:0:0:
70,222,76318,5,0,0:0:0:0:
111,222,76429,1,0,0:0:0:0:
134,253,76540,6,0,P|135:298|126:314,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
21,384,76762,2,0,P|124:354|260:391,1,224.999996423721,0|0,0:0|0:0,0:0:0:0:
384,366,77207,22,0,L|394:268,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
499,62,77429,2,0,L|486:135,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
507,237,77651,2,0,P|450:231|388:184,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
404,203,77874,2,0,L|313:217,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
113,212,78096,6,0,P|128:267|111:328,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
115,319,78318,2,0,L|213:340,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
274,371,78540,38,0,L|257:186,1,179.999991645813,8|0,3:0|0:0,0:0:0:0:
128,139,78874,1,0,0:0:0:0:
128,139,78985,6,0,L|230:128,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
365,34,79207,37,4,0:0:0:0:
430,114,79318,1,0,0:0:0:0:
361,184,79429,2,0,P|304:170|277:110,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
278,126,79651,2,0,L|189:133,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
64,263,79874,6,0,B|37:230|37:230|50:143,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
66,119,80096,2,0,L|80:210,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
71,361,80318,38,0,B|135:350|135:350|182:305|182:305|243:297,1,179.999991645813,8|0,3:0|0:0,0:0:0:0:
302,247,80651,1,0,0:0:0:0:
222,211,80762,1,0,3:0:0:0:
478,344,80985,5,4,0:0:0:0:
491,309,81096,5,0,0:0:0:0:
498,265,81207,5,8,3:0:0:0:
485,223,81318,5,0,0:0:0:0:
458,179,81429,5,4,0:0:0:0:
418,147,81540,5,0,0:0:0:0:
352,126,81651,5,0,3:0:0:0:
281,149,81762,5,0,0:0:0:0:
239,221,81874,5,4,0:0:0:0:
159,262,81985,5,0,0:0:0:0:
66,234,82096,5,8,3:0:0:0:
11,145,82207,5,0,0:0:0:0:
55,33,82318,5,4,0:0:0:0:
273,44,82540,37,0,3:0:0:0:
320,103,82651,1,0,0:0:0:0:
394,118,82762,1,4,0:0:0:0:
468,100,82874,1,0,0:0:0:0:
507,36,82985,1,8,3:0:0:0:
495,19,83207,5,4,0:0:0:0:
335,83,83318,1,0,0:0:0:0:
453,81,83429,1,0,3:0:0:0:
283,24,83540,2,0,P|196:37|141:120,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
60,238,83874,1,8,3:0:0:0:
21,164,83985,2,0,P|59:149|175:193,1,149.999997615814,0|8,0:0|3:0,0:0:0:0:
252,206,84318,38,0,P|271:160|264:125,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
139,257,84540,2,0,P|131:302|149:340,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
240,379,84762,2,0,B|330:360|330:360|298:344,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
312,351,84985,2,0,P|279:321|270:287,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
359,165,85207,6,0,B|389:202|389:202|368:282,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
373,265,85429,2,0,L|454:282,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
498,139,85651,38,0,P|446:120|396:0,1,179.999991645813,8|0,3:0|0:0,0:0:0:0:
394,13,85985,1,0,0:0:0:0:
301,92,86096,6,0,L|214:83,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
66,66,86318,1,4,0:0:0:0:
13,136,86429,1,0,0:0:0:0:
72,193,86540,2,0,P|120:210|190:178,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
176,192,86762,2,0,P|154:237|160:288,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
309,370,86985,37,0,3:0:0:0:
359,310,87096,1,0,0:0:0:0:
283,297,87207,2,0,L|203:318,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
4,203,87429,2,0,B|55:211|55:211|82:255|82:255|134:266,1,149.999997615814,8|0,3:0|0:0,0:0:0:0:
238,217,87762,1,0,0:0:0:0:
183,120,87874,6,0,L|89:111,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
98,33,88096,2,0,L|23:26,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
306,182,88318,38,0,L|400:173,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
391,95,88540,2,0,L|465:88,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
232,28,88762,2,0,L|220:92,1,37.4999994039535,0|0,3:0|0:0,0:0:0:0:
243,39,88874,2,0,L|231:103,1,37.4999994039535,0|0,0:0|0:0,0:0:0:0:
256,50,88985,2,0,L|251:87,1,37.4999994039535,4|0,0:0|0:0,0:0:0:0:
485,87,89207,6,0,L|493:51,3,37.4999994039535,8|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
396,120,89429,2,0,L|411:197,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
471,317,89651,38,0,P|411:299|320:336,1,149.999997615814,0|4,3:0|0:0,0:0:0:0:
61,239,90096,2,0,P|121:221|212:258,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
367,21,90540,6,0,P|336:57|328:104,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
163,96,90762,2,0,P|194:132|202:179,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
190,346,90985,37,8,3:0:0:0:
328,272,91096,1,0,0:0:0:0:
154,272,91207,5,8,3:0:0:0:
365,338,91318,1,0,0:0:0:0:
257,382,91429,38,0,B|290:333|224:286|269:219,1,149.999997615814,4|4,3:0|0:0,0:0:0:0:
325,196,91762,1,0,0:0:0:0:
325,196,91874,2,0,P|365:210|436:184,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
430,190,92096,2,0,B|418:110,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
313,19,92318,2,0,L|190:36,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
201,34,92540,2,0,B|214:117,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
209,252,92762,5,8,3:0:0:0:
156,261,92874,1,0,0:0:0:0:
112,231,92985,1,4,0:0:0:0:
60,222,93096,1,0,0:0:0:0:
13,247,93207,38,0,P|4:288|19:328,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
173,186,93429,1,4,0:0:0:0:
215,120,93540,1,0,0:0:0:0:
162,49,93651,2,0,P|125:39|76:61,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
234,138,93874,2,0,P|273:157|313:148,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
385,39,94096,5,0,3:0:0:0:
337,286,94318,2,0,L|322:373,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
409,327,94540,2,0,P|418:277|280:230,1,224.999996423721,8|0,3:0|0:0,0:0:0:0:
239,319,94985,2,0,P|218:357|173:373,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
34,344,95207,37,4,0:0:0:0:
21,309,95318,5,0,0:0:0:0:
14,265,95429,5,8,3:0:0:0:
27,223,95540,5,0,0:0:0:0:
54,179,95651,5,4,0:0:0:0:
94,147,95762,5,0,0:0:0:0:
160,126,95873,5,0,3:0:0:0:
231,149,95984,5,0,0:0:0:0:
273,221,96096,5,4,0:0:0:0:
353,262,96207,5,0,0:0:0:0:
446,234,96318,5,8,3:0:0:0:
501,145,96429,5,0,0:0:0:0:
450,36,96540,5,4,0:0:0:0:
239,44,96762,5,0,3:0:0:0:
192,103,96873,1,0,0:0:0:0:
118,118,96984,1,4,0:0:0:0:
44,100,97096,1,0,0:0:0:0:
5,36,97207,1,8,3:0:0:0:
17,19,97429,37,4,0:0:0:0:
146,51,97540,1,0,0:0:0:0:
29,122,97651,2,0,L|39:193,1,56.2499991059302,0|0,3:0|0:0,0:0:0:0:
44,197,97874,6,0,P|100:231|176:201,1,112.500002503395,4|0,0:0|0:0,0:0:0:0:
301,160,98096,38,0,P|329:140|382:137,1,84.3750018775463,8|0,3:0|0:0,0:0:0:0:
398,147,98318,6,0,B|431:187|431:187|415:279,1,112.500002503395,4|0,0:0|0:0,0:0:0:0:
265,371,98540,38,0,L|180:361,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
127,202,98762,2,0,L|141:113,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
193,260,98985,2,0,P|144:291|68:278,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
91,290,99207,2,0,L|79:373,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
20,184,99429,6,0,B|4:141|4:141|27:66,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
23,78,99651,2,0,L|109:91,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
271,74,99874,2,0,P|254:31|222:12,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
186,180,100096,2,0,P|232:175|260:147,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
132,63,100318,37,0,3:0:0:0:
253,157,100540,1,4,0:0:0:0:
285,167,100651,1,0,0:0:0:0:
357,129,100762,5,8,3:0:0:0:
389,139,100873,1,0,0:0:0:0:
422,148,100985,2,0,P|407:200|416:233,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
459,377,101207,38,0,P|472:333|459:295,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
398,242,101429,2,0,L|314:257,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
165,354,101651,2,0,P|116:332|211:264,1,224.999996423721,8|0,3:0|0:0,0:0:0:0:
302,165,102096,6,0,L|292:89,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
392,91,102318,2,0,L|382:14,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
192,229,102540,38,0,L|212:136,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
107,172,102762,2,0,L|127:79,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
314,332,102985,6,0,L|305:278,1,37.4999994039535,0|0,3:0|0:0,0:0:0:0:
343,345,103096,2,0,L|334:291,1,37.4999994039535,0|0,0:0|0:0,0:0:0:0:
370,358,103207,2,0,L|361:304,1,37.4999994039535,4|0,0:0|0:0,0:0:0:0:
380,117,103429,38,0,L|374:75,3,37.4999994039535,8|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
444,166,103651,2,0,P|417:188|346:191,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
392,2,103874,2,0,P|424:14|462:74,1,74.999998807907,4|0,3:0|0:0,0:0:0:0:
271,129,104096,2,0,P|265:94|298:31,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
505,113,104318,5,8,3:0:0:0:
269,217,104540,38,0,L|216:216,3,37.4999994039535,0|0|0|0,3:0|3:0|3:0|3:0,0:0:0:0:
360,220,104762,1,0,3:0:0:0:
296,384,104874,1,4,3:0:0:0:
102,307,105096,5,0,0:0:0:0:
102,307,105207,2,0,B|206:381|258:244|374:330,1,269.999987468719,12|0,3:0|0:0,0:0:0:0:
439,319,105651,6,0,P|379:336|396:236,1,168.750003755093,0|0,3:0|0:0,0:0:0:0:
373,258,106096,6,0,P|374:315|443:283,1,112.500002503395,8|0,3:0|0:0,0:0:0:0:
420,323,106651,37,0,0:0:0:0:
469,245,106763,1,4,0:0:0:0:
508,322,106874,1,0,0:0:0:0:
379,245,106985,1,8,3:0:0:0:
483,105,107207,6,0,L|474:40,3,56.2500012516975,4|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
462,30,107429,38,0,P|401:56|319:25,1,149.999997615814,0|0,3:0|0:0,0:0:0:0:
272,120,107874,2,0,P|184:91|118:125,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
103,213,108207,2,0,B|128:232|128:232|269:200,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
393,187,108540,2,0,L|385:286,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
333,338,108763,1,8,3:0:0:0:
467,307,108874,6,0,L|509:297,2,37.4999994039535,0|0|4,0:0|0:0|0:0,0:0:0:0:
409,380,109096,1,0,0:0:0:0:
300,257,109207,38,0,P|279:218|281:171,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
401,118,109429,1,4,0:0:0:0:
401,118,109651,6,0,L|315:109,1,74.999998807907,8|4,3:0|0:0,0:0:0:0:
256,15,109985,37,0,0:0:0:0:
175,121,110096,2,0,P|162:60|109:16,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
128,26,110318,2,0,P|106:86|47:122,1,112.49999821186,0|0,0:0|0:0,0:0:0:0:
69,114,110540,2,0,P|135:105|185:131,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
160,223,110762,6,0,B|142:230|142:230|120:228|120:228|95:239|95:239|71:235|71:235|49:244|49:244|22:249,1,112.500002503395,4|0,0:0|0:0,0:0:0:0:
193,334,110985,38,0,P|216:310|242:301,1,56.2500012516975,0|0,3:0|0:0,0:0:0:0:
335,325,111207,2,0,P|366:353|378:379,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
273,383,111429,2,0,L|304:213,1,168.750003755093,0|0,0:0|0:0,0:0:0:0:
383,255,111874,22,0,B|422:273|422:273|476:273,1,74.999998807907,8|0,3:0|3:0,0:0:0:0:
209,219,112096,2,0,B|169:221|169:221|131:206,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
403,147,112318,2,0,B|352:114|352:114|337:43|337:43|295:109|295:109|234:115,1,269.999987468719,8|0,3:0|0:0,0:0:0:0:

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,6 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.IO;
@@ -19,23 +18,9 @@ namespace osu.Game.Rulesets.Osu.Tests
public class StackingTest
{
[Test]
public void TestStacking()
public void TestStackingEdgeCaseOne()
{
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(beatmap_data)))
using (var reader = new LineBufferedReader(stream))
{
var beatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
var converted = new TestWorkingBeatmap(beatmap).GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>());
var objects = converted.HitObjects.ToList();
// The last hitobject triggers the stacking
for (int i = 0; i < objects.Count - 1; i++)
Assert.AreEqual(0, ((OsuHitObject)objects[i]).StackHeight);
}
}
private const string beatmap_data = @"
using (var stream = new MemoryStream(@"
osu file format v14
[General]
@@ -62,6 +47,65 @@ SliderTickRate:0.5
311,185,218471,2,0,L|325:209,1,25
311,185,218671,2,0,L|304:212,1,25
311,185,240271,5,0,0:0:0:0:
";
"u8.ToArray()))
using (var reader = new LineBufferedReader(stream))
{
var beatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
var converted = new TestWorkingBeatmap(beatmap).GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>());
var objects = converted.HitObjects.ToList();
// The last hitobject triggers the stacking
for (int i = 0; i < objects.Count - 1; i++)
Assert.AreEqual(0, ((OsuHitObject)objects[i]).StackHeight);
}
}
[Test]
public void TestStackingEdgeCaseTwo()
{
using (var stream = new MemoryStream(@"
osu file format v14
// extracted from https://osu.ppy.sh/beatmapsets/365006#osu/801165
[General]
StackLeniency: 0.2
[Difficulty]
HPDrainRate:6
CircleSize:4
OverallDifficulty:8
ApproachRate:9.3
SliderMultiplier:2
SliderTickRate:1
[TimingPoints]
5338,444.444444444444,4,2,0,50,1,0
82893,-76.9230769230769,4,2,8,50,0,0
85115,-76.9230769230769,4,2,0,50,0,0
85337,-100,4,2,8,60,0,0
85893,-100,4,2,7,60,0,0
86226,-100,4,2,8,60,0,0
88893,-58.8235294117647,4,1,8,70,0,1
[HitObjects]
427,124,84226,1,0,3:0:0:0:
427,124,84337,1,0,3:0:0:0:
427,124,84449,1,8,0:0:0:0:
"u8.ToArray()))
using (var reader = new LineBufferedReader(stream))
{
var beatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
var converted = new TestWorkingBeatmap(beatmap).GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>());
var objects = converted.HitObjects.ToList();
Assert.That(objects, Has.Count.EqualTo(3));
// The last hitobject triggers the stacking
for (int i = 0; i < objects.Count - 1; i++)
Assert.AreEqual(0, ((OsuHitObject)objects[i]).StackHeight);
}
}
}
}

View File

@@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
continue;
double endTime = stackBaseObject.GetEndTime();
double stackThreshold = objectN.TimePreempt * beatmap.StackLeniency;
float stackThreshold = calculateStackThreshold(beatmap, objectN);
if (objectN.StartTime - endTime > stackThreshold)
// We are no longer within stacking range of the next object.
@@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
OsuHitObject objectI = hitObjects[i];
if (objectI.StackHeight != 0 || objectI is Spinner) continue;
double stackThreshold = objectI.TimePreempt * beatmap.StackLeniency;
float stackThreshold = calculateStackThreshold(beatmap, objectI);
/* If this object is a hitcircle, then we enter this "special" case.
* It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider.
@@ -151,7 +151,10 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
double endTime = objectN.GetEndTime();
if (objectI.StartTime - endTime > stackThreshold)
// truncation to integer is required to match stable
// compare https://github.com/peppy/osu-stable-reference/blob/08e3dafd525934cf48880b08e91c24ce4ad8b761/osu!/GameplayElements/HitObjectManager.cs#L1725
// - both quantities being subtracted there are integers
if ((int)objectI.StartTime - (int)endTime > stackThreshold)
// We are no longer within stacking range of the previous object.
break;
@@ -232,7 +235,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
for (int j = i + 1; j < hitObjects.Count; j++)
{
double stackThreshold = hitObjects[i].TimePreempt * beatmap.StackLeniency;
float stackThreshold = calculateStackThreshold(beatmap, hitObjects[i]);
if (hitObjects[j].StartTime - stackThreshold > startTime)
break;
@@ -264,5 +267,17 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
}
}
}
/// <remarks>
/// Truncation of <see cref="OsuHitObject.TimePreempt"/> to <see cref="int"/>, as well as keeping the result as <see cref="float"/>, are both done
/// <a href="https://github.com/peppy/osu-stable-reference/blob/08e3dafd525934cf48880b08e91c24ce4ad8b761/osu!/GameplayElements/HitObjectManager.cs#L1652">
/// for the purposes of stable compatibility
/// </a>.
/// Note that for top-level objects <see cref="OsuHitObject.TimePreempt"/> is supposed to be integral anyway;
/// see <see cref="OsuHitObject.ApplyDefaultsToSelf"/> using <see cref="IBeatmapDifficultyInfo.DifficultyRangeInt"/> when calculating it.
/// Slider ticks and end circles are the exception to that, but they do not matter for stacking.
/// </remarks>
private static float calculateStackThreshold(IBeatmap beatmap, OsuHitObject hitObject)
=> (int)hitObject.TimePreempt * beatmap.StackLeniency;
}
}

View File

@@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Objects
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_RANGE);
TimePreempt = IBeatmapDifficultyInfo.DifficultyRangeInt(difficulty.ApproachRate, PREEMPT_RANGE);
// Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR.
// This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above.

View File

@@ -412,7 +412,8 @@ namespace osu.Game.Rulesets.Osu
Description = "Affects how early objects appear on screen relative to their hit time.",
AdditionalMetrics =
[
new RulesetBeatmapAttribute.AdditionalMetric("Approach time", LocalisableString.Interpolate($@"{IBeatmapDifficultyInfo.DifficultyRange(effectiveDifficulty.ApproachRate, OsuHitObject.PREEMPT_RANGE):#,0.##} ms"))
new RulesetBeatmapAttribute.AdditionalMetric("Approach time",
LocalisableString.Interpolate($@"{IBeatmapDifficultyInfo.DifficultyRangeInt(effectiveDifficulty.ApproachRate, OsuHitObject.PREEMPT_RANGE):#,0.##} ms"))
]
};

View File

@@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
/// </summary>
protected bool AllowPartRotation { get; set; }
private Vector2 cursorScale;
private Vector2 cursorScale = Vector2.One;
public Vector2 CursorScale
{
@@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
float distance = diff.Length;
Vector2 direction = diff / distance;
float interval = Texture.DisplayWidth / 2.5f * IntervalMultiplier;
float interval = Texture.DisplayWidth * CursorScale.X / 2.5f * IntervalMultiplier;
float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0);
for (float d = interval; d < stopAt; d += interval)

View File

@@ -44,7 +44,13 @@ namespace osu.Game.Tests.Online
availableBeatmap = importedSet.Beatmaps[0];
unavailableBeatmap = importedSet.Beatmaps[1];
Realm.Write(r => r.Remove(r.Find<BeatmapInfo>(unavailableBeatmap.ID)!));
Realm.Write(r =>
{
BeatmapInfo available = r.Find<BeatmapInfo>(availableBeatmap.ID)!;
available.OnlineMD5Hash = available.MD5Hash;
r.Remove(r.Find<BeatmapInfo>(unavailableBeatmap.ID)!);
});
}
public override void SetUpSteps()

View File

@@ -27,6 +27,7 @@ using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
using Realms;
namespace osu.Game.Tests.Online
{
@@ -229,6 +230,14 @@ namespace osu.Game.Tests.Online
return testBeatmapManager.CurrentImport = base.ImportModel(item, archive, parameters, cancellationToken);
}
protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters)
{
foreach (var beatmap in model.Beatmaps)
beatmap.OnlineMD5Hash = beatmap.MD5Hash;
base.PostImport(model, realm, parameters);
}
}
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Game.Graphics.Cursor;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
using osu.Game.Tests.Visual.UserInterface;
namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneFormSampleSet : ThemeComparisonTestScene
{
public TestSceneFormSampleSet()
: base(false)
{
}
protected override Drawable CreateContent() => new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FormSampleSet
{
Current =
{
Value = new EditorBeatmapSkin.SampleSet(3, "Custom set #3")
{
Filenames = ["normal-hitwhistle3.wav"]
}
},
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.4f,
}
}
};
}
}

View File

@@ -0,0 +1,271 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Tests.Visual.UserInterface;
using osuTK;
namespace osu.Game.Tests.Visual.Settings
{
public partial class TestSceneSettingsItemV2 : ThemeComparisonTestScene
{
private readonly Bindable<SettingsNote.Data?> note = new Bindable<SettingsNote.Data?>();
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
private FormSliderBar<float> sliderBar = null!;
private FormSliderBar<float> classicSliderBar = null!;
private SearchContainer searchContainer = null!;
public TestSceneSettingsItemV2()
: base(false)
{
}
protected override Drawable CreateContent()
{
return new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new BackgroundBox
{
RelativeSizeAxes = Axes.Both,
},
new OsuContextMenuContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 400,
RelativeSizeAxes = Axes.Y,
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = searchContainer = new SearchContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Vertical,
Spacing = new Vector2(7),
Padding = new MarginPadding { Vertical = 10 },
Children = new[]
{
new SettingsItemV2(new FormTextBox
{
Caption = "Artist",
HintText = "Poot artist here!",
PlaceholderText = "Here is an artist",
Current = { Value = string.Empty, Default = string.Empty }
}),
new SettingsItemV2(new FormTextBox
{
Caption = "Artist",
HintText = "Poot artist here!",
PlaceholderText = "Here is an artist",
Current = { Value = string.Empty, Default = string.Empty, Disabled = true }
}),
new SettingsItemV2(new FormNumberBox(allowDecimals: true)
{
Caption = "Number",
HintText = "Insert your favourite number",
PlaceholderText = "Mine is 42!",
Current = { Value = string.Empty, Default = string.Empty }
}),
new SettingsItemV2(new FormCheckBox
{
Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
})
{
Note = { BindTarget = note },
},
new SettingsItemV2(new FormCheckBox
{
Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
Current = { Disabled = true },
}),
new SettingsItemV2(new FormCheckBox
{
Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
Current = { Value = true, Disabled = true },
}),
new SettingsItemV2(new FormEnumDropdown<CountdownType>
{
Caption = EditorSetupStrings.EnableCountdown,
HintText = EditorSetupStrings.CountdownDescription,
}),
new SettingsItemV2(new FormEnumDropdown<CountdownType>
{
Caption = EditorSetupStrings.EnableCountdown,
HintText = EditorSetupStrings.CountdownDescription,
Current = { Disabled = true },
}),
new SettingsItemV2(new FormEnumDropdown<Language>
{
Caption = "Dropdown with many items",
HintText = EditorSetupStrings.CountdownDescription,
})
{
Note = { BindTarget = note },
},
new SettingsItemV2(sliderBar = new FormSliderBar<float>
{
Caption = "Slider",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
},
}),
new SettingsItemV2(new FormSliderBar<float>
{
Caption = "Slider",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
Disabled = true,
},
TransferValueOnCommit = true,
}),
new SettingsItemV2(new FormSliderBar<float>
{
Caption = "Slider without revert button",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
},
})
{
ShowRevertToDefaultButton = false
},
new SettingsItemV2(classicSliderBar = new FormSliderBar<float>
{
Caption = "Slider with classic default",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
},
})
{
ApplyClassicDefault = () => classicSliderBar.Current.Value = 2,
},
},
},
},
}
},
},
};
}
[Test]
public void TestDisplay()
{
AddStep("display", () => CreateThemedContent(OverlayColourScheme.Purple));
}
[Test]
public void TestNote()
{
AddStep("set informational note", () => note.Value = new SettingsNote.Data(LayoutSettingsStrings.OsuIsRunningExclusiveFullscreen.ToString(), SettingsNote.Type.Informational));
AddStep("set warning note",
() => note.Value = new SettingsNote.Data(
"Using unlimited frame limiter can lead to stutters, bad performance and overheating. It will not improve perceived latency. “2x refresh rate” is recommended.",
SettingsNote.Type.Warning));
AddStep("set critical note",
() => note.Value = new SettingsNote.Data(
"You have done something so horrible in the game settings to the point we have invented a new note type for this. Look at it, it's in red. It's worse than yellow.",
SettingsNote.Type.Critical));
AddStep("clear note", () => note.Value = null);
}
[Test]
public void TestClassicDefault()
{
AddStep("modify irrelevant setting", () => sliderBar.Current.Value = 4);
AddStep("apply classic defaults", () => this.ChildrenOfType<ISettingsItem>().Where(i => i.HasClassicDefault).ForEach(s => s.ApplyClassicDefault()));
AddStep("apply regular defaults", () => this.ChildrenOfType<ISettingsItem>().Where(i => i.HasClassicDefault).ForEach(s => s.ApplyDefault()));
AddStep("set classic filter", () => searchContainer.SearchTerm = SettingsItemV2.CLASSIC_DEFAULT_SEARCH_TERM);
AddStep("apply classic defaults", () => this.ChildrenOfType<ISettingsItem>().Where(i => i.HasClassicDefault).ForEach(s => s.ApplyClassicDefault()));
AddStep("apply regular defaults", () => this.ChildrenOfType<ISettingsItem>().Where(i => i.HasClassicDefault).ForEach(s => s.ApplyDefault()));
AddStep("set no filter", () => searchContainer.SearchTerm = string.Empty);
AddAssert("irrelevant setting left out", () => sliderBar.Current.Value, () => Is.EqualTo(4));
}
/// <summary>
/// Ensures that the reset to default button uses the correct implementation of IsDefault to determine whether it should be shown or not.
/// Values have been chosen so that after being set, Value != Default (but they are close enough that the difference is negligible compared to Precision).
/// </summary>
[TestCase(4.2f)]
[TestCase(9.9f)]
public void TestRestoreDefaultValueButtonPrecision(float initialValue)
{
BindableFloat current = null!;
SettingsRevertToDefaultButton revertToDefaultButton = null!;
AddStep("set current bindable", () => sliderBar.Current = current = new BindableFloat(initialValue)
{
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
});
AddStep("retrieve restore default button", () => revertToDefaultButton = sliderBar.FindClosestParent<SettingsItemV2>().ChildrenOfType<SettingsRevertToDefaultButton>().Single());
AddAssert("restore button hidden", () => revertToDefaultButton.X == 0);
AddStep("change value to next closest", () => sliderBar.Current.Value += current.Precision * 0.6f);
AddUntilStep("restore button shown", () => revertToDefaultButton.X > 0);
AddStep("restore default", () => sliderBar.Current.SetDefault());
AddUntilStep("restore button hidden", () => revertToDefaultButton.X == 0);
}
private partial class BackgroundBox : Box
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Colour = colourProvider.Background4;
}
}
}
}

View File

@@ -17,6 +17,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osuTK.Input;
using FilterControl = osu.Game.Screens.SelectV2.FilterControl;
using NoResultsPlaceholder = osu.Game.Screens.SelectV2.NoResultsPlaceholder;
@@ -354,6 +355,30 @@ namespace osu.Game.Tests.Visual.SongSelectV2
checkMatchedBeatmaps(1);
}
[Test]
public void TestScopeToBeatmapWhenDifficultiesSplitApart()
{
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortBy(SortMode.Difficulty);
checkMatchedBeatmaps(6);
AddUntilStep("wait for spread indicator", () => this.ChildrenOfType<PanelBeatmapStandalone.SpreadDisplay>().Any(d => d.Enabled.Value));
AddStep("click spread indicator", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PanelBeatmapStandalone.SpreadDisplay>().Single(d => d.Enabled.Value));
InputManager.Click(MouseButton.Left);
});
WaitForFiltering();
checkMatchedBeatmaps(3);
AddStep("press Escape", () => InputManager.Key(Key.Escape));
WaitForFiltering();
checkMatchedBeatmaps(6);
}
private NoResultsPlaceholder? getPlaceholder() => SongSelect.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault();
private void checkMatchedBeatmaps(int expected) => AddUntilStep($"{expected} matching shown", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected));

View File

@@ -1,15 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Screens.Edit.Setup;
using osuTK;
@@ -25,109 +28,199 @@ namespace osu.Game.Tests.Visual.UserInterface
protected override Drawable CreateContent() => new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = new PopoverContainer
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Child = new OsuScrollContainer
new BackgroundBox
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
},
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new OsuScrollContainer
{
AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 400,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Padding = new MarginPadding(10),
Children = new Drawable[]
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
new FormTextBox
AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 400,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Padding = new MarginPadding(10),
Children = new Drawable[]
{
Caption = "Artist",
HintText = "Poot artist here!",
PlaceholderText = "Here is an artist",
TabbableContentContainer = this,
},
new FormTextBox
{
Caption = "Artist",
HintText = "Poot artist here!",
PlaceholderText = "Here is an artist",
Current = { Disabled = true },
TabbableContentContainer = this,
},
new FormNumberBox(allowDecimals: true)
{
Caption = "Number",
HintText = "Insert your favourite number",
PlaceholderText = "Mine is 42!",
TabbableContentContainer = this,
},
new FormCheckBox
{
Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
},
new FormCheckBox
{
Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
Current = { Disabled = true },
},
new FormSliderBar<float>
{
Caption = "Slider",
Current = new BindableFloat
new FormTextBox
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
Caption = "Artist",
HintText = "Poot artist here!",
PlaceholderText = "Here is an artist",
TabbableContentContainer = this,
},
TabbableContentContainer = this,
},
new FormEnumDropdown<CountdownType>
{
Caption = EditorSetupStrings.EnableCountdown,
HintText = EditorSetupStrings.CountdownDescription,
},
new FormFileSelector
{
Caption = "File selector",
PlaceholderText = "Select a file",
},
new FormBeatmapFileSelector(true)
{
Caption = "File selector with intermediate choice dialog",
PlaceholderText = "Select a file",
},
new FormColourPalette
{
Caption = "Combo colours",
Colours =
new FormTextBox
{
Colour4.Red,
Colour4.Green,
Colour4.Blue,
Colour4.Yellow,
}
},
new FormButton
{
Caption = "No text in button",
Action = () => { },
},
new FormButton
{
Caption = "Text in button which is pretty long and is very likely to wrap",
ButtonText = "Foo the bar",
Action = () => { },
Caption = "Artist",
HintText = "Poot artist here!",
PlaceholderText = "Here is an artist",
Current = { Disabled = true },
TabbableContentContainer = this,
},
new FormNumberBox(allowDecimals: true)
{
Caption = "Number",
HintText = "Insert your favourite number",
PlaceholderText = "Mine is 42!",
TabbableContentContainer = this,
},
new FormCheckBox
{
Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
},
new FormCheckBox
{
Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
Current = { Disabled = true },
},
new FormCheckBox
{
Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
Current = { Value = true, Disabled = true },
},
new FormSliderBar<float>
{
Caption = "Slider",
HintText = "Slider hint",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
},
TabbableContentContainer = this,
},
new FormSliderBar<float>
{
Caption = "Slider",
HintText = "Slider hint",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
Disabled = true,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
new FormSliderBar<float>
{
Caption = "Slider (percentage)",
HintText = "Percentage slider hint",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 1,
Value = 0.2f,
Precision = 0.0001f,
},
DisplayAsPercentage = true,
TabbableContentContainer = this,
},
new FormSliderBar<float>
{
Caption = "Slider (custom)",
HintText = "Custom slider hint",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 1,
Value = 0.2f,
Precision = 0.0001f,
},
LabelFormat = v => $"{v * 100:0.00} funometer",
TooltipFormat = v => $"This setting has the value set to {v * 100:0.00} funometer.",
TabbableContentContainer = this,
},
new FormSliderBar<float>
{
Caption = "Slider (custom)",
HintText = "Custom slider hint",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 1,
Value = 0.2f,
Precision = 0.0001f,
Disabled = true,
},
TransferValueOnCommit = true,
LabelFormat = v => $"{v * 100:0.00} funometer",
TooltipFormat = v => $"This setting has the value set to {v * 100:0.00} funometer.",
TabbableContentContainer = this,
},
new FormEnumDropdown<CountdownType>
{
Caption = EditorSetupStrings.EnableCountdown,
HintText = EditorSetupStrings.CountdownDescription,
},
new FormEnumDropdown<CountdownType>
{
Caption = EditorSetupStrings.EnableCountdown,
HintText = EditorSetupStrings.CountdownDescription,
Current = { Disabled = true },
},
new FormFileSelector
{
Caption = "File selector",
PlaceholderText = "Select a file",
},
new FormBeatmapFileSelector(true)
{
Caption = "File selector with intermediate choice dialog",
PlaceholderText = "Select a file",
},
new FormColourPalette
{
Caption = "Combo colours",
Colours =
{
Colour4.Red,
Colour4.Green,
Colour4.Blue,
Colour4.Yellow,
}
},
new FormButton
{
Caption = "No text in button",
Action = () => { },
},
new FormButton
{
Caption = "Text in button which is pretty long and is very likely to wrap",
ButtonText = "Foo the bar",
Action = () => { },
},
},
},
},
},
}
}
};
private partial class BackgroundBox : Box
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Colour = colourProvider.Background4;
}
}
}
}

View File

@@ -107,5 +107,105 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("slider is default", () => slider.Current.IsDefault);
}
[Test]
public void TestDisabled()
{
FormSliderBar<float> slider = null!;
AddStep("create content", () =>
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
{
slider = new FormSliderBar<float>
{
Caption = "Slider",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
Default = 5f,
}
},
}
};
});
AddStep("set slider to 1", () => slider.Current.Value = 1);
AddStep("disable slider", () => slider.Current.Disabled = true);
AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType<Circle>().Single()));
AddStep("double click nub", () =>
{
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
});
AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1));
AddStep("click on textbox part", () =>
{
InputManager.MoveMouseTo(slider.ChildrenOfType<FormTextBox.InnerTextBox>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("no text selected", () => slider.ChildrenOfType<FormTextBox.InnerTextBox>().Single().SelectedText, () => Is.Empty);
AddStep("attempt to input text", () =>
{
InputManager.Key(Key.Number4);
InputManager.Key(Key.Enter);
});
AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1));
}
[Test]
public void TestDisabledImmediately()
{
FormSliderBar<float> slider = null!;
AddStep("create content", () =>
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
{
slider = new FormSliderBar<float>
{
Caption = "Slider",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
Default = 5f,
Disabled = true,
},
TransferValueOnCommit = true,
},
}
};
});
AddStep("click on textbox part", () =>
{
InputManager.MoveMouseTo(slider.ChildrenOfType<FormTextBox.InnerTextBox>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("no text selected", () => slider.ChildrenOfType<FormTextBox.InnerTextBox>().Single().SelectedText, () => Is.Empty);
}
}
}

View File

@@ -2,14 +2,19 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneLabelledSwitchButton : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
[TestCase(false)]
[TestCase(true)]
public void TestSwitchButton(bool hasDescription) => createSwitchButton(hasDescription);

View File

@@ -4,15 +4,21 @@
#nullable disable
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneSwitchButton : OsuManualInputManagerTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
private SwitchButton switchButton;
[SetUp]
@@ -42,5 +48,15 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("toggle bindable", () => bindable.Toggle());
AddStep("toggle bindable", () => bindable.Toggle());
}
[Test]
public void TestDisabledState()
{
AddToggleStep("toggle disabled", v =>
{
if (switchButton.IsNotNull())
switchButton.Current.Disabled = v;
});
}
}
}

View File

@@ -8,7 +8,6 @@ using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.API;
@@ -56,7 +55,7 @@ namespace osu.Game.Tournament.Screens.Setup
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.2f),
Colour = ColourProvider.Background5,
},
new OsuScrollContainer
{

View File

@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Screens
@@ -15,6 +16,9 @@ namespace osu.Game.Tournament.Screens
[Resolved]
protected LadderInfo LadderInfo { get; private set; } = null!;
[Cached]
protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
protected TournamentScreen()
{
RelativeSizeAxes = Axes.Both;

View File

@@ -328,6 +328,14 @@ namespace osu.Game.Beatmaps
.Filter(query, arguments)
.FirstOrDefault()?.Detach());
/// <summary>
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s for a specific online ID.
/// </summary>
/// <returns>A matching local beatmap info if existing and in a valid state.</returns>
public BeatmapInfo? QueryOnlineBeatmapId(int id) => Realm.Run(r =>
r.All<BeatmapInfo>()
.ForOnlineId(id).SingleOrDefault()?.Detach());
/// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
/// </summary>
@@ -476,9 +484,11 @@ namespace osu.Game.Beatmaps
public Task<ExternalEditOperation<BeatmapSetInfo>> BeginExternalEditing(BeatmapSetInfo model) =>
beatmapImporter.BeginExternalEditing(model);
public Task Export(BeatmapSetInfo beatmap) => beatmapExporter.ExportAsync(beatmap.ToLive(Realm));
public Task Export(BeatmapSetInfo beatmapSet) => beatmapExporter.ExportAsync(beatmapSet.ToLive(Realm));
public Task ExportLegacy(BeatmapSetInfo beatmap) => legacyBeatmapExporter.ExportAsync(beatmap.ToLive(Realm));
public Task ExportLegacy(BeatmapSetInfo beatmapSet) => legacyBeatmapExporter.ExportAsync(beatmapSet.ToLive(Realm));
public Task ExportLegacy(BeatmapInfo beatmap) => legacyBeatmapExporter.ExportAsync(beatmap.ToLive(Realm));
private void updateHashAndMarkDirty(BeatmapSetInfo setInfo)
{

View File

@@ -95,6 +95,31 @@ namespace osu.Game.Beatmaps
static double DifficultyRange(double difficulty, DifficultyRange range)
=> DifficultyRange(difficulty, range.Min, range.Mid, range.Max);
/// <summary>
/// Maps a difficulty value [0, 10] to a two-piece linear range of values.
/// Floors the value to `int`, usually to match osu!stable spec.
/// </summary>
/// <param name="difficulty">The difficulty value to be mapped.</param>
/// <param name="range">The values that define the two linear ranges.
/// <list type="table">
/// <item>
/// <term>od0</term>
/// <description>Minimum of the resulting range which will be achieved by a difficulty value of 0.</description>
/// </item>
/// <item>
/// <term>od5</term>
/// <description>Midpoint of the resulting range which will be achieved by a difficulty value of 5.</description>
/// </item>
/// <item>
/// <term>od10</term>
/// <description>Maximum of the resulting range which will be achieved by a difficulty value of 10.</description>
/// </item>
/// </list>
/// </param>
/// <returns>Value to which the difficulty value maps in the specified range.</returns>
static int DifficultyRangeInt(double difficulty, DifficultyRange range)
=> (int)DifficultyRange(difficulty, range.Min, range.Mid, range.Max);
/// <summary>
/// Inverse function to <see cref="DifficultyRange(double,double,double,double)"/>.
/// Maps a value returned by the function above back to the difficulty that produced it.

View File

@@ -2,17 +2,22 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Timing;
using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Database
@@ -175,5 +180,54 @@ namespace osu.Game.Database
}
protected override string FileExtension => @".osz";
public Task ExportAsync(Live<BeatmapInfo> beatmap) => Task.Run(() =>
{
string itemFilename = Path.GetFileNameWithoutExtension(beatmap.PerformRead(s => s.File!.Filename.GetValidFilename()));
const string osu_extension = @".osu";
if (itemFilename.Length > MAX_FILENAME_LENGTH - osu_extension.Length)
itemFilename = itemFilename.Remove(MAX_FILENAME_LENGTH - osu_extension.Length);
IEnumerable<string> existingExports = ExportStorage
.GetFiles(string.Empty, $"{itemFilename}*{osu_extension}")
.Concat(ExportStorage.GetDirectories(string.Empty));
string filename = NamingUtils.GetNextBestFilename(existingExports, $"{itemFilename}{osu_extension}");
ProgressNotification notification = new ProgressNotification
{
State = ProgressNotificationState.Active,
Text = $"Exporting {itemFilename}...",
};
PostNotification?.Invoke(notification);
try
{
beatmap.PerformRead(b =>
{
using var exportStream = ExportStorage.CreateFileSafely(filename);
using var inputFile = GetFileContents(b.BeatmapSet!, b.File!);
if (inputFile == null)
throw new InvalidOperationException($"Beatmap file {b.File!.Filename} could not be opened!");
inputFile.CopyTo(exportStream);
});
}
catch
{
notification.State = ProgressNotificationState.Cancelled;
// cleanup if export is failed or canceled.
ExportStorage.Delete(filename);
throw;
}
notification.CompletionText = $"Exported {itemFilename}! Click to view.";
notification.CompletionClickAction = () => ExportStorage.PresentFileExternally(filename);
notification.State = ProgressNotificationState.Completed;
});
}
}

View File

@@ -41,13 +41,13 @@ namespace osu.Game.Database
protected abstract string FileExtension { get; }
protected readonly Storage UserFileStorage;
private readonly Storage exportStorage;
protected readonly Storage ExportStorage;
public Action<Notification>? PostNotification { get; set; }
protected LegacyExporter(Storage storage)
{
exportStorage = (storage as OsuStorage)?.GetExportStorage() ?? storage.GetStorageForDirectory(@"exports");
ExportStorage = (storage as OsuStorage)?.GetExportStorage() ?? storage.GetStorageForDirectory(@"exports");
UserFileStorage = storage.GetStorageForDirectory(@"files");
}
@@ -74,9 +74,9 @@ namespace osu.Game.Database
if (itemFilename.Length > MAX_FILENAME_LENGTH - FileExtension.Length)
itemFilename = itemFilename.Remove(MAX_FILENAME_LENGTH - FileExtension.Length);
IEnumerable<string> existingExports = exportStorage
IEnumerable<string> existingExports = ExportStorage
.GetFiles(string.Empty, $"{itemFilename}*{FileExtension}")
.Concat(exportStorage.GetDirectories(string.Empty));
.Concat(ExportStorage.GetDirectories(string.Empty));
string filename = NamingUtils.GetNextBestFilename(existingExports, $"{itemFilename}{FileExtension}");
@@ -92,7 +92,7 @@ namespace osu.Game.Database
try
{
using (var stream = exportStorage.CreateFileSafely(filename))
using (var stream = ExportStorage.CreateFileSafely(filename))
{
await ExportToStreamAsync(model, stream, notification, linkedSource.Token).ConfigureAwait(false);
}
@@ -102,12 +102,12 @@ namespace osu.Game.Database
notification.State = ProgressNotificationState.Cancelled;
// cleanup if export is failed or canceled.
exportStorage.Delete(filename);
ExportStorage.Delete(filename);
throw;
}
notification.CompletionText = $"Exported {itemFilename}! Click to view.";
notification.CompletionClickAction = () => exportStorage.PresentFileExternally(filename);
notification.CompletionClickAction = () => ExportStorage.PresentFileExternally(filename);
notification.State = ProgressNotificationState.Completed;
}

View File

@@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using Realms;
namespace osu.Game.Database
@@ -102,5 +104,13 @@ namespace osu.Game.Database
/// Quite often we only care about changes at a collection level. This can be used to guard and early-return when no such changes are in a callback.
/// </remarks>
public static bool HasCollectionChanges(this ChangeSet changes) => changes.InsertedIndices.Length > 0 || changes.DeletedIndices.Length > 0 || changes.Moves.Length > 0;
public static IQueryable<BeatmapInfo> NotDeleted(this IQueryable<BeatmapInfo> beatmaps) =>
beatmaps.Filter($@"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false");
public static IQueryable<BeatmapInfo> ForOnlineId(this IQueryable<BeatmapInfo> beatmaps, int id) =>
beatmaps
.NotDeleted()
.Filter($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", id);
}
}

View File

@@ -28,6 +28,7 @@ namespace osu.Game.Graphics
public static IconUsage EditCircle => get(OsuIconMapping.EditCircle);
public static IconUsage LeftCircle => get(OsuIconMapping.LeftCircle);
public static IconUsage RightCircle => get(OsuIconMapping.RightCircle);
public static IconUsage Undo => get(OsuIconMapping.Undo);
public static IconUsage Audio => get(OsuIconMapping.Audio);
public static IconUsage Beatmap => get(OsuIconMapping.Beatmap);
@@ -386,6 +387,9 @@ namespace osu.Game.Graphics
[Description(@"twitter")]
Twitter,
[Description(@"undo")]
Undo,
[Description(@"user-interface")]
UserInterface,

View File

@@ -25,12 +25,12 @@ namespace osu.Game.Graphics.UserInterface
/// </summary>
public bool DisplayAsPercentage { get; set; }
public virtual LocalisableString TooltipText { get; private set; }
public virtual LocalisableString TooltipText { get; protected set; }
/// <summary>
/// Maximum number of decimal digits to be displayed in the tooltip.
/// </summary>
private const int max_decimal_digits = 5;
public const int MAX_DECIMAL_DIGITS = 5;
private Sample sample = null!;
@@ -46,7 +46,7 @@ namespace osu.Game.Graphics.UserInterface
protected override void LoadComplete()
{
base.LoadComplete();
CurrentNumber.BindValueChanged(current => TooltipText = GetDisplayableValue(current.NewValue), true);
CurrentNumber.BindValueChanged(current => TooltipText = GetTooltipText(current.NewValue), true);
}
protected override void OnUserChange(T value)
@@ -55,7 +55,7 @@ namespace osu.Game.Graphics.UserInterface
playSample(value);
TooltipText = GetDisplayableValue(value);
TooltipText = GetTooltipText(value);
}
private void playSample(T value)
@@ -83,6 +83,6 @@ namespace osu.Game.Graphics.UserInterface
channel.Play();
}
public LocalisableString GetDisplayableValue(T value) => value.ToStandardFormattedString(max_decimal_digits, DisplayAsPercentage);
protected virtual LocalisableString GetTooltipText(T value) => value.ToStandardFormattedString(MAX_DECIMAL_DIGITS, DisplayAsPercentage);
}
}

View File

@@ -1,10 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -13,13 +16,12 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class FormCheckBox : CompositeDrawable, IHasCurrentValue<bool>
public partial class FormCheckBox : CompositeDrawable, IHasCurrentValue<bool>, IFormControl
{
public Bindable<bool> Current
{
@@ -42,10 +44,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
private Box background = null!;
private FormFieldCaption caption = null!;
private OsuSpriteText text = null!;
private Nub checkbox = null!;
private Sample? sampleChecked;
private Sample? sampleUnchecked;
private Sample? sampleDisabled;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
@@ -86,7 +88,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
},
checkbox = new Nub
new SwitchButton
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
@@ -98,6 +100,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
sampleChecked = audio.Samples.Get(@"UI/check-on");
sampleUnchecked = audio.Samples.Get(@"UI/check-off");
sampleDisabled = audio.Samples.Get(@"UI/default-select-disabled");
}
protected override void LoadComplete()
@@ -109,6 +112,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
updateState();
playSamples();
background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint);
ValueChanged?.Invoke();
});
current.BindDisabledChanged(_ => updateState(), true);
}
@@ -137,25 +142,36 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
if (!Current.Disabled)
Current.Value = !Current.Value;
else
sampleDisabled?.Play();
return true;
}
private void updateState()
{
background.Colour = Current.Disabled ? colourProvider.Background4 : colourProvider.Background5;
caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
checkbox.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
text.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
caption.Colour = Current.Disabled ? colourProvider.Background1 : colourProvider.Content2;
text.Colour = Current.Disabled ? colourProvider.Background1 : colourProvider.Content1;
text.Text = Current.Value ? CommonStrings.Enabled : CommonStrings.Disabled;
if (!Current.Disabled)
{
BorderThickness = IsHovered ? 2 : 0;
// use FadeColour to override any existing colour transform (i.e. FlashColour on click).
background.FadeColour(IsHovered
? ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4)
: colourProvider.Background5);
if (IsHovered)
BorderColour = colourProvider.Light4;
}
BorderThickness = IsHovered ? 2 : 0;
BorderColour = Current.Disabled ? colourProvider.Dark1 : colourProvider.Light4;
}
public IEnumerable<LocalisableString> FilterTerms => Caption.Yield();
public event Action? ValueChanged;
public bool IsDefault => Current.IsDefault;
public void SetDefault() => Current.SetDefault();
public bool IsDisabled => Current.Disabled;
}
}

View File

@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
@@ -17,7 +18,7 @@ using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class FormDropdown<T> : OsuDropdown<T>
public partial class FormDropdown<T> : OsuDropdown<T>, IFormControl
{
/// <summary>
/// Caption describing this slider bar, displayed on top of the controls.
@@ -29,6 +30,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
/// </summary>
public LocalisableString HintText { get; init; }
/// <summary>
/// The maximum height of the dropdown's menu.
/// By default, this is set to 200px high. Set to <see cref="float.PositiveInfinity"/> to remove such limit.
/// </summary>
public float MaxHeight { get; set; } = 200;
private FormDropdownHeader header = null!;
[BackgroundDependencyLoader]
@@ -40,12 +47,40 @@ namespace osu.Game.Graphics.UserInterfaceV2
header.HintText = HintText;
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(_ => ValueChanged?.Invoke());
}
public virtual IEnumerable<LocalisableString> FilterTerms
{
get
{
yield return Caption;
foreach (var item in MenuItems)
yield return item.Text.Value;
}
}
public event Action? ValueChanged;
public bool IsDefault => Current.IsDefault;
public void SetDefault() => Current.SetDefault();
public bool IsDisabled => Current.Disabled;
protected override DropdownHeader CreateHeader() => header = new FormDropdownHeader
{
Dropdown = this,
};
protected override DropdownMenu CreateMenu() => new FormDropdownMenu();
protected override DropdownMenu CreateMenu() => new FormDropdownMenu
{
MaxHeight = MaxHeight,
};
private partial class FormDropdownHeader : DropdownHeader
{
@@ -136,10 +171,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Size = new Vector2(16),
Margin = new MarginPadding { Right = 5 },
},
};
AddInternal(new HoverClickSounds());
AddInternal(new HoverClickSounds
{
Enabled = { BindTarget = Enabled },
});
}
protected override void LoadComplete()
@@ -175,29 +214,26 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
label.Alpha = string.IsNullOrEmpty(SearchBar.SearchTerm.Value) ? 1 : 0;
caption.Colour = Dropdown.Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
label.Colour = Dropdown.Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
chevron.Colour = Dropdown.Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
caption.Colour = Dropdown.Current.Disabled ? colourProvider.Background1 : colourProvider.Content2;
label.Colour = Dropdown.Current.Disabled ? colourProvider.Background1 : colourProvider.Content1;
chevron.Colour = Dropdown.Current.Disabled ? colourProvider.Background1 : colourProvider.Content1;
DisabledColour = Colour4.White;
bool dropdownOpen = Dropdown.Menu.State == MenuState.Open;
if (!Dropdown.Current.Disabled)
{
BorderThickness = IsHovered || dropdownOpen ? 2 : 0;
BorderThickness = IsHovered || dropdownOpen ? 2 : 0;
if (Dropdown.Current.Disabled)
BorderColour = colourProvider.Dark1;
else
BorderColour = dropdownOpen ? colourProvider.Highlight1 : colourProvider.Light4;
if (dropdownOpen)
Background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
else if (IsHovered)
Background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
else
Background.Colour = colourProvider.Background5;
}
if (dropdownOpen)
Background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
else if (IsHovered)
Background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
else
{
Background.Colour = colourProvider.Background4;
}
Background.Colour = colourProvider.Background5;
}
private void updateChevron()

View File

@@ -252,7 +252,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
return popover;
}
protected partial class FileChooserPopover : OsuPopover
public partial class FileChooserPopover : OsuPopover
{
protected override string PopInSampleName => "UI/overlay-big-pop-in";
protected override string PopOutSampleName => "UI/overlay-big-pop-out";

View File

@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using osu.Framework.Allocation;
@@ -16,13 +17,15 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Extensions;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class FormSliderBar<T> : CompositeDrawable, IHasCurrentValue<T>
public partial class FormSliderBar<T> : CompositeDrawable, IHasCurrentValue<T>, IFormControl
where T : struct, INumber<T>, IMinMaxValue<T>
{
public Bindable<T> Current
@@ -96,9 +99,31 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
}
/// <summary>
/// Whether to format the tooltip as a percentage or the actual value.
/// </summary>
public bool DisplayAsPercentage { get; init; }
/// <summary>
/// Whether sound effects should play when adjusting this slider.
/// </summary>
public bool PlaySamplesOnAdjust { get; init; }
/// <summary>
/// The string formatting function to use for the value label.
/// </summary>
public Func<T, LocalisableString> LabelFormat { get; init; }
/// <summary>
/// The string formatting function to use for the slider's tooltip text.
/// If not provided, <see cref="LabelFormat"/> is used.
/// </summary>
public Func<T, LocalisableString> TooltipFormat { get; init; }
private Box background = null!;
private Box flashLayer = null!;
private FormTextBox.InnerTextBox textBox = null!;
private OsuSpriteText valueLabel = null!;
private InnerSlider slider = null!;
private FormFieldCaption captionText = null!;
private IFocusManager focusManager = null!;
@@ -108,6 +133,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
private readonly Bindable<Language> currentLanguage = new Bindable<Language>();
public FormSliderBar()
{
LabelFormat ??= defaultLabelFormat;
TooltipFormat ??= v => LabelFormat(v);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, OsuGame? game)
{
@@ -152,6 +183,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
// the textbox is hidden when the control is unfocused,
// but clicking on the label should reach the textbox,
// therefore make it always present.
AlwaysPresent = true,
CommitOnFocusLost = true,
SelectAllOnFocus = true,
OnInputError = () =>
@@ -161,6 +196,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
},
TabbableContentContainer = tabbableContentContainer,
},
valueLabel = new TruncatingSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
Padding = new MarginPadding { Right = 5 },
},
slider = new InnerSlider
{
Anchor = Anchor.CentreRight,
@@ -169,6 +212,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
Width = 0.5f,
Current = currentNumberInstantaneous,
OnCommit = () => current.Value = currentNumberInstantaneous.Value,
TooltipFormat = TooltipFormat,
DisplayAsPercentage = DisplayAsPercentage,
PlaySamplesOnAdjust = PlaySamplesOnAdjust,
}
},
},
@@ -194,7 +240,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
slider.IsDragging.BindValueChanged(_ => updateState());
slider.Focused.BindValueChanged(_ => updateState());
current.ValueChanged += e => currentNumberInstantaneous.Value = e.NewValue;
current.ValueChanged += e =>
{
currentNumberInstantaneous.Value = e.NewValue;
ValueChanged?.Invoke();
};
current.MinValueChanged += v => currentNumberInstantaneous.MinValue = v;
current.MaxValueChanged += v => currentNumberInstantaneous.MaxValue = v;
current.PrecisionChanged += v => currentNumberInstantaneous.Precision = v;
@@ -207,10 +258,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
currentNumberInstantaneous.Disabled = disabled;
updateState();
};
current.CopyTo(currentNumberInstantaneous);
currentLanguage.BindValueChanged(_ => Schedule(updateValueDisplay));
currentNumberInstantaneous.BindDisabledChanged(_ => updateState());
currentNumberInstantaneous.BindValueChanged(e =>
{
if (!TransferValueOnCommit)
@@ -283,7 +336,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override bool OnClick(ClickEvent e)
{
focusManager.ChangeFocus(textBox);
if (!Current.Disabled)
focusManager.ChangeFocus(textBox);
return true;
}
@@ -291,14 +345,20 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
bool childHasFocus = slider.Focused.Value || textBox.Focused.Value;
textBox.Alpha = 1;
textBox.ReadOnly = currentNumberInstantaneous.Disabled;
textBox.Alpha = textBox.Focused.Value ? 1 : 0;
valueLabel.Alpha = textBox.Focused.Value ? 0 : 1;
background.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Background4 : colourProvider.Background5;
captionText.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
textBox.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
captionText.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Background1 : colourProvider.Content2;
textBox.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Background1 : colourProvider.Content1;
valueLabel.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Background1 : colourProvider.Content1;
BorderThickness = childHasFocus || IsHovered || slider.IsDragging.Value ? 2 : 0;
BorderColour = childHasFocus ? colourProvider.Highlight1 : colourProvider.Light4;
if (Current.Disabled)
BorderColour = colourProvider.Dark1;
else
BorderColour = childHasFocus ? colourProvider.Highlight1 : colourProvider.Light4;
if (childHasFocus)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
@@ -312,19 +372,28 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
if (updatingFromTextBox) return;
textBox.Text = slider.GetDisplayableValue(currentNumberInstantaneous.Value).ToString();
textBox.Text = currentNumberInstantaneous.Value.ToStandardFormattedString(OsuSliderBar<T>.MAX_DECIMAL_DIGITS);
valueLabel.Text = LabelFormat(currentNumberInstantaneous.Value);
}
private LocalisableString defaultLabelFormat(T value) => currentNumberInstantaneous.Value.ToStandardFormattedString(OsuSliderBar<T>.MAX_DECIMAL_DIGITS, DisplayAsPercentage);
private partial class InnerSlider : OsuSliderBar<T>
{
public BindableBool Focused { get; } = new BindableBool();
public BindableBool IsDragging { get; set; } = new BindableBool();
public Action? OnCommit { get; set; }
public sealed override LocalisableString TooltipText => base.TooltipText;
public required Func<T, LocalisableString> TooltipFormat { get; init; }
private Box leftBox = null!;
private Box rightBox = null!;
private InnerSliderNub nub = null!;
private HoverClickSounds sounds = null!;
public const float NUB_WIDTH = 10;
[Resolved]
@@ -373,14 +442,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
}
},
new HoverClickSounds()
sounds = new HoverClickSounds()
};
}
protected override void LoadComplete()
{
base.LoadComplete();
updateState();
Current.BindDisabledChanged(_ => updateState(), true);
}
protected override void UpdateAfterChildren()
@@ -433,9 +502,19 @@ namespace osu.Game.Graphics.UserInterfaceV2
private void updateState()
{
sounds.Enabled.Value = !Current.Disabled;
rightBox.Colour = colourProvider.Background6;
leftBox.Colour = HasFocus || IsHovered || IsDragged ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Dark2;
nub.Colour = HasFocus || IsHovered || IsDragged ? colourProvider.Highlight1 : colourProvider.Light4;
if (Current.Disabled)
{
leftBox.Colour = colourProvider.Dark3;
nub.Colour = colourProvider.Dark1;
}
else
{
leftBox.Colour = HasFocus || IsHovered || IsDragged ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Highlight1.Opacity(0.3f);
nub.Colour = HasFocus || IsHovered || IsDragged ? colourProvider.Highlight1 : colourProvider.Light4;
}
}
protected override void UpdateValue(float value)
@@ -452,6 +531,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
return result;
}
protected sealed override LocalisableString GetTooltipText(T value) => TooltipFormat(value);
}
private partial class InnerSliderNub : Circle
@@ -475,5 +556,15 @@ namespace osu.Game.Graphics.UserInterfaceV2
return true;
}
}
public IEnumerable<LocalisableString> FilterTerms => new[] { Caption, HintText };
public event Action? ValueChanged;
public bool IsDefault => Current.IsDefault;
public void SetDefault() => Current.SetDefault();
public bool IsDisabled => Current.Disabled;
}
}

View File

@@ -2,9 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -20,7 +22,7 @@ using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class FormTextBox : CompositeDrawable, IHasCurrentValue<string>
public partial class FormTextBox : CompositeDrawable, IHasCurrentValue<string>, IFormControl
{
public Bindable<string> Current
{
@@ -157,6 +159,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
focusManager = GetContainingFocusManager()!;
textBox.Focused.BindValueChanged(_ => updateState());
current.BindValueChanged(_ => ValueChanged?.Invoke());
current.BindDisabledChanged(_ => updateState(), true);
}
@@ -185,26 +189,22 @@ namespace osu.Game.Graphics.UserInterfaceV2
textBox.ReadOnly = disabled;
textBox.Alpha = 1;
caption.Colour = disabled ? colourProvider.Foreground1 : colourProvider.Content2;
caption.Colour = disabled ? colourProvider.Background1 : colourProvider.Content2;
textBox.Colour = disabled ? colourProvider.Foreground1 : colourProvider.Content1;
if (!disabled)
{
BorderThickness = IsHovered || textBox.Focused.Value ? 2 : 0;
BorderThickness = IsHovered || textBox.Focused.Value ? 2 : 0;
if (disabled)
BorderColour = colourProvider.Dark1;
else
BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4;
if (textBox.Focused.Value)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
else if (IsHovered)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
else
background.Colour = colourProvider.Background5;
}
if (textBox.Focused.Value)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
else if (IsHovered)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
else
{
BorderThickness = 0;
background.Colour = colourProvider.Background4;
}
background.Colour = colourProvider.Background5;
}
internal partial class InnerTextBox : OsuTextBox
@@ -247,5 +247,15 @@ namespace osu.Game.Graphics.UserInterfaceV2
OnInputError?.Invoke();
}
}
public event Action? ValueChanged;
public bool IsDefault => current.IsDefault;
public void SetDefault() => current.SetDefault();
public bool IsDisabled => current.Disabled;
public IEnumerable<LocalisableString> FilterTerms => Caption.Yield();
}
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Graphics.UserInterfaceV2
{
/// <summary>
/// Represents an interface for all form controls.
/// </summary>
public interface IFormControl : IDrawable, IHasFilterTerms
{
/// <summary>
/// Invoked when the value of the control has changed.
/// </summary>
event Action ValueChanged;
/// <summary>
/// Whether the value of this control is in a default state.
/// </summary>
bool IsDefault { get; }
/// <summary>
/// If enabled, resets the control to its default state.
/// </summary>
void SetDefault();
/// <summary>
/// Whether the control is currently disabled.
/// </summary>
bool IsDisabled { get; }
}
}

View File

@@ -4,7 +4,6 @@
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -24,12 +23,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
private const float padding = 1.25f;
private readonly Box fill;
private readonly Container switchContainer;
private readonly Drawable switchCircle;
private readonly CircularBorderContainer circularContainer;
private readonly Container nubContainer;
private readonly Drawable nub;
private readonly CircularContainer content;
private Color4 enabledColour;
private Color4 disabledColour;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
private Sample? sampleChecked;
private Sample? sampleUnchecked;
@@ -38,7 +37,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
Size = new Vector2(45, 20);
InternalChild = circularContainer = new CircularBorderContainer
InternalChild = content = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
BorderColour = Color4.White,
@@ -56,15 +55,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(border_thickness + padding),
Child = switchContainer = new Container
Child = nubContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Child = switchCircle = new CircularContainer
Child = nub = new Circle
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Masking = true,
Child = new Box { RelativeSizeAxes = Axes.Both }
}
}
}
@@ -73,14 +71,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider, OsuColour colours, AudioManager audio)
private void load(AudioManager audio)
{
enabledColour = colourProvider?.Highlight1 ?? colours.BlueDark;
disabledColour = colourProvider?.Background3 ?? colours.Gray3;
switchContainer.Colour = enabledColour;
fill.Colour = disabledColour;
sampleChecked = audio.Samples.Get(@"UI/check-on");
sampleUnchecked = audio.Samples.Get(@"UI/check-off");
}
@@ -89,27 +81,29 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
base.LoadComplete();
Current.BindValueChanged(updateState, true);
Current.BindDisabledChanged(_ => updateColours());
Current.BindValueChanged(_ => updateState(), true);
FinishTransforms(true);
}
private void updateState(ValueChangedEvent<bool> state)
private void updateState()
{
switchCircle.MoveToX(state.NewValue ? switchContainer.DrawWidth - switchCircle.DrawWidth : 0, 200, Easing.OutQuint);
fill.FadeTo(state.NewValue ? 1 : 0, 250, Easing.OutQuint);
nub.MoveToX(Current.Value ? nubContainer.DrawWidth - nub.DrawWidth : 0, 200, Easing.OutQuint);
fill.FadeTo(Current.Value ? 1 : 0, 250, Easing.OutQuint);
updateBorder();
updateColours();
}
protected override bool OnHover(HoverEvent e)
{
updateBorder();
updateColours();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateBorder();
updateColours();
base.OnHoverLost(e);
}
@@ -123,15 +117,35 @@ namespace osu.Game.Graphics.UserInterfaceV2
sampleUnchecked?.Play();
}
private void updateBorder()
private void updateColours()
{
circularContainer.TransformBorderTo((Current.Value ? enabledColour : disabledColour).Lighten(IsHovered ? 0.3f : 0));
}
ColourInfo borderColour;
ColourInfo switchColour;
private partial class CircularBorderContainer : CircularContainer
{
public void TransformBorderTo(ColourInfo colour)
=> this.TransformTo(nameof(BorderColour), colour, 250, Easing.OutQuint);
if (Current.Disabled)
{
borderColour = colourProvider.Dark2;
switchColour = colourProvider.Dark1;
fill.Colour = colourProvider.Dark5;
}
else
{
bool hover = IsHovered && !Current.Disabled;
borderColour = hover ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Highlight1.Opacity(0.3f);
switchColour = hover ? colourProvider.Highlight1 : colourProvider.Light4;
if (!Current.Value)
{
borderColour = borderColour.MultiplyAlpha(0.8f);
switchColour = switchColour.MultiplyAlpha(0.8f);
}
fill.Colour = colourProvider.Background6;
}
nubContainer.FadeColour(switchColour, 250, Easing.OutQuint);
content.TransformTo(nameof(BorderColour), borderColour, 250, Easing.OutQuint);
}
}
}

View File

@@ -54,6 +54,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString ExportForCompatibility => new TranslatableString(getKey(@"export_for_compatibility"), @"For compatibility (.osz)");
/// <summary>
/// "Guest difficulty (.osu)"
/// </summary>
public static LocalisableString ExportGuestDifficulty => new TranslatableString(getKey(@"export_guest_difficulty"), @"Guest difficulty (.osu)");
/// <summary>
/// "Create new difficulty"
/// </summary>

View File

@@ -259,6 +259,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString NoMatchingBeatmapsDescription => new TranslatableString(getKey(@"no_matching_beatmaps_description"), @"No beatmaps match your filter criteria!");
/// <summary>
/// "Temporarily showing all beatmaps in"
/// </summary>
public static LocalisableString TemporarilyShowingAllBeatmapsIn => new TranslatableString(getKey(@"temporarily_showing_all_beatmaps_in"), @"Temporarily showing all beatmaps in");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@@ -0,0 +1,178 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Overlays.Settings
{
public sealed partial class SettingsItemV2 : CompositeDrawable, ISettingsItem, IConditionalFilterable
{
private readonly IFormControl control;
private readonly SettingsRevertToDefaultButton revertButton;
private readonly BindableBool controlDefault = new BindableBool(true);
private readonly BindableBool controlEnabled = new BindableBool(true);
/// <summary>
/// Whether a revert button should be displayed when the control is modified away from default state.
/// </summary>
public bool ShowRevertToDefaultButton { get; init; } = true;
/// <summary>
/// A note to display underneath the setting.
/// </summary>
public readonly Bindable<SettingsNote.Data?> Note = new Bindable<SettingsNote.Data?>();
public SettingsItemV2(IFormControl control)
{
this.control = control;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS_RIGHT },
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new[]
{
revertButton = new SettingsRevertToDefaultButton
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Action = ApplyDefault,
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = (Drawable)control,
}
}
},
new SettingsNote
{
RelativeSizeAxes = Axes.X,
Current = { BindTarget = Note },
},
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
controlDefault.Value = control.IsDefault;
controlEnabled.Value = !control.IsDisabled;
controlDefault.BindValueChanged(_ => updateDefaultState());
controlEnabled.BindValueChanged(_ => updateDefaultState(), true);
FinishTransforms(true);
}
private void updateDefaultState()
{
bool showRevertButton = !controlDefault.Value && controlEnabled.Value && ShowRevertToDefaultButton;
if (showRevertButton)
revertButton.Show();
else
revertButton.Hide();
}
protected override void Update()
{
base.Update();
controlDefault.Value = control.IsDefault;
controlEnabled.Value = !control.IsDisabled;
}
#region ISettingsItem
public bool HasClassicDefault => ApplyClassicDefault != null;
/// <summary>
/// If set, this setting is considered as having a "classic" default value,
/// and this is the function for overwriting the control with that value.
/// </summary>
public Action? ApplyClassicDefault { get; set; }
void ISettingsItem.ApplyClassicDefault() => ApplyClassicDefault?.Invoke();
public void ApplyDefault()
{
if (!control.IsDisabled)
control.SetDefault();
}
public event Action SettingChanged
{
add => control.ValueChanged += value;
remove => control.ValueChanged -= value;
}
#endregion
#region Filtering
public const string CLASSIC_DEFAULT_SEARCH_TERM = @"has-classic-default";
public IEnumerable<string> Keywords { get; init; } = Enumerable.Empty<string>();
public IEnumerable<LocalisableString> FilterTerms
{
get
{
var filterTerms = new List<LocalisableString>(Keywords.Select(k => (LocalisableString)k));
filterTerms.AddRange(control.FilterTerms);
if (HasClassicDefault)
filterTerms.Add(CLASSIC_DEFAULT_SEARCH_TERM);
return filterTerms;
}
}
private bool matchingFilter = true;
public bool MatchingFilter
{
get => matchingFilter;
set
{
bool wasPresent = IsPresent;
matchingFilter = value;
if (IsPresent != wasPresent)
Invalidate(Invalidation.Presence);
}
}
public override bool IsPresent => base.IsPresent && MatchingFilter;
public bool FilteringActive { get; set; }
public BindableBool CanBeShown { get; } = new BindableBool(true);
IBindable<bool> IConditionalFilterable.CanBeShown => CanBeShown;
#endregion
}
}

View File

@@ -0,0 +1,118 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osuTK.Graphics;
namespace osu.Game.Overlays.Settings
{
public sealed partial class SettingsNote : CompositeDrawable
{
public readonly Bindable<Data?> Current = new Bindable<Data?>();
private Box background = null!;
private OsuTextFlowContainer text = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeDuration = 300;
AutoSizeEasing = Easing.OutQuint;
InternalChild = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Top = 5, Bottom = 5 },
Child = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
CornerRadius = 5,
CornerExponent = 2.5f,
Masking = true,
Children = new Drawable[]
{
background = new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
},
text = new OsuTextFlowContainer(s => s.Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold))
{
Padding = new MarginPadding(8),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(_ => updateDisplay(), true);
FinishTransforms(true);
}
private void updateDisplay()
{
// Explicitly use ClearTransforms to clear any existing auto-size transform before modifying size / flag.
ClearTransforms();
if (Current.Value == null)
{
AutoSizeAxes = Axes.None;
this.ResizeHeightTo(0, 300, Easing.OutQuint);
this.FadeOut(250, Easing.OutQuint);
return;
}
AutoSizeAxes = Axes.Y;
this.FadeIn(250, Easing.OutQuint);
switch (Current.Value.Type)
{
case Type.Informational:
background.Colour = colourProvider.Dark2;
text.Colour = colourProvider.Content2;
break;
case Type.Warning:
background.Colour = colours.Orange1;
text.Colour = colourProvider.Background5;
break;
case Type.Critical:
background.Colour = colours.Red1;
text.Colour = colourProvider.Background5;
break;
}
text.Text = Current.Value.Text;
}
public record Data(LocalisableString Text, Type Type);
public enum Type
{
Informational,
Warning,
Critical,
}
}
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Localisation;
using osuTK;
namespace osu.Game.Overlays.Settings
{
public partial class SettingsRevertToDefaultButton : OsuClickableContainer
{
public const float WIDTH = 32;
public float IconSize { get; init; } = 14;
private Box background = null!;
private SpriteIcon spriteIcon = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
// this is done to ensure a click on this button doesn't trigger focus on a parent element which contains the button.
public override bool AcceptsFocus => true;
public SettingsRevertToDefaultButton()
{
Size = new Vector2(WIDTH, 50);
}
[BackgroundDependencyLoader]
private void load()
{
Masking = true;
CornerRadius = 5;
CornerExponent = 2.5f;
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background3,
},
spriteIcon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = colourProvider.Light1,
Icon = OsuIcon.Undo,
Margin = new MarginPadding { Left = 12, Right = 5 },
Size = new Vector2(IconSize),
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Enabled.BindValueChanged(_ => updateDisplay(), true);
}
public override LocalisableString TooltipText => CommonStrings.RevertToDefault;
protected override bool OnHover(HoverEvent e)
{
updateDisplay();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateDisplay();
base.OnHoverLost(e);
}
public override void Show()
{
this.FadeIn().MoveToX(WIDTH - 10, 200, Easing.OutElasticQuarter);
}
public override void Hide()
{
this.MoveToX(0, 120, Easing.OutExpo).Then().FadeOut();
}
private void updateDisplay()
{
spriteIcon.FadeColour(IsHovered ? colourProvider.Content2 : colourProvider.Light1, 300, Easing.OutQuint);
background.FadeColour(IsHovered ? colourProvider.Background2 : colourProvider.Background3, 300, Easing.OutQuint);
}
}
}

View File

@@ -31,6 +31,9 @@ namespace osu.Game.Overlays
{
public const float CONTENT_MARGINS = 20;
// extra margin to give room to the revert-to-default button in settings controls.
public const float CONTENT_MARGINS_RIGHT = 30;
public const float TRANSITION_LENGTH = 600;
private const float sidebar_width = SettingsSidebar.EXPANDED_WIDTH;

View File

@@ -0,0 +1,347 @@
// 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.IO;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components
{
public partial class FormSampleSet : CompositeDrawable, IHasCurrentValue<EditorBeatmapSkin.SampleSet?>
{
public Bindable<EditorBeatmapSkin.SampleSet?> Current
{
get => current.Current;
set => current.Current = value;
}
public Func<FileInfo, string>? SampleAddRequested { get; init; }
public Action<string>? SampleRemoveRequested { get; init; }
private readonly BindableWithCurrent<EditorBeatmapSkin.SampleSet?> current = new BindableWithCurrent<EditorBeatmapSkin.SampleSet?>();
private readonly Dictionary<(string name, string bank), SampleButton> buttons = new Dictionary<(string, string), SampleButton>();
private Box background = null!;
private FormFieldCaption caption = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 5;
InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(9),
Spacing = new Vector2(7),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
caption = new FormFieldCaption(),
new GridContainer
{
AutoSizeAxes = Axes.Both,
RowDimensions = Enumerable.Repeat(new Dimension(GridSizeMode.AutoSize), 4).ToArray(),
ColumnDimensions = Enumerable.Repeat(new Dimension(GridSizeMode.AutoSize), 5).ToArray(),
Content = createTableContent().ToArray(),
}
},
},
};
}
private IEnumerable<Drawable[]> createTableContent()
{
string[] columns = HitSampleInfo.ALL_ADDITIONS.Prepend(HitSampleInfo.HIT_NORMAL).ToArray();
string[] rows = HitSampleInfo.ALL_BANKS;
yield return columns.Select(makeTableHeading).Prepend(Empty()).ToArray();
foreach (string row in rows)
{
List<Drawable> drawables = [makeTableHeading(row)];
foreach (string col in columns)
drawables.Add(buttons[(col, row)] = makeButton());
yield return drawables.ToArray();
}
}
private OsuSpriteText makeTableHeading(string text) => new OsuSpriteText
{
Text = text,
Font = OsuFont.Style.Caption1,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
private SampleButton makeButton() => new SampleButton
{
Width = 60,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding(5),
SampleAddRequested = SampleAddRequested,
SampleRemoveRequested = SampleRemoveRequested,
};
protected override void LoadComplete()
{
base.LoadComplete();
updateState();
Current.BindValueChanged(setChanged, true);
}
private void setChanged(ValueChangedEvent<EditorBeatmapSkin.SampleSet?> valueChangedEvent)
{
var set = valueChangedEvent.NewValue;
caption.Caption = set?.Name ?? default(LocalisableString);
Alpha = set != null && set.SampleSetIndex > 0 ? 1 : 0;
if (set != null)
{
foreach (var (sample, button) in buttons)
{
button.ExpectedFilename.Value = $@"{sample.bank}-{sample.name}{(set.SampleSetIndex > 1 ? set.SampleSetIndex : null)}";
button.ActualFilename.Value = set.FindSampleIfExists(sample.name, sample.bank);
}
}
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateState();
base.OnHoverLost(e);
}
private void updateState()
{
background.Colour = colourProvider.Background5;
caption.Colour = colourProvider.Content2;
BorderThickness = IsHovered ? 2 : 0;
if (IsHovered)
BorderColour = colourProvider.Light4;
}
public partial class SampleButton : OsuButton, IHasPopover, IHasContextMenu
{
/// <summary>
/// The expected filename for the sample that this button represents.
/// Does not contain extension.
/// </summary>
public Bindable<string> ExpectedFilename { get; } = new Bindable<string>();
/// <summary>
/// The actual chosen filename for the sample that this button represent.
/// Can be <see langword="null"/> if the sample is omitted / missing.
/// Does contain extension.
/// </summary>
public Bindable<string?> ActualFilename { get; } = new Bindable<string?>();
/// <summary>
/// Invoked when a new sample is selected via this button.
/// </summary>
public Func<FileInfo, string>? SampleAddRequested { get; init; }
/// <summary>
/// Invoked when a sample removal is selected via this button.
/// </summary>
public Action<string>? SampleRemoveRequested { get; init; }
private Bindable<FileInfo?> selectedFile { get; } = new Bindable<FileInfo?>();
private TrianglesV2? triangles { get; set; }
protected override float HoverLayerFinalAlpha => 0;
private Color4? triangleGradientSecondColour;
private SpriteIcon icon = null!;
[Resolved]
private OverlayColourProvider overlayColourProvider { get; set; } = null!;
[Resolved]
private EditorBeatmap? editorBeatmap { get; set; }
private HoverSounds? hoverSounds;
private ISample? sample;
public SampleButton()
: base(null)
{
}
[BackgroundDependencyLoader]
private void load()
{
Add(icon = new SpriteIcon
{
Icon = FontAwesome.Solid.Plus,
Size = new Vector2(16),
Shadow = true,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
Action = () =>
{
if (ActualFilename.Value == null)
{
selectedFile.Value = null;
this.ShowPopover();
}
else
sample?.Play();
};
if (editorBeatmap?.BeatmapSkin != null)
editorBeatmap.BeatmapSkin.BeatmapSkinChanged += recycleSamples;
}
protected override void LoadComplete()
{
base.LoadComplete();
Content.CornerRadius = 4;
Add(triangles = new TrianglesV2
{
Thickness = 0.02f,
SpawnRatio = 0.6f,
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue,
});
ActualFilename.BindValueChanged(_ => updateState(), true);
selectedFile.BindValueChanged(_ => addSample());
}
private void updateState()
{
BackgroundColour = ActualFilename.Value == null ? overlayColourProvider.Background3 : overlayColourProvider.Colour3;
triangleGradientSecondColour = BackgroundColour.Lighten(0.2f);
icon.Icon = ActualFilename.Value == null ? FontAwesome.Solid.Plus : FontAwesome.Solid.Play;
recycleSamples();
if (triangles == null)
return;
triangles.Colour = ColourInfo.GradientVertical(triangleGradientSecondColour.Value, BackgroundColour);
}
private void recycleSamples()
{
if (hoverSounds?.Parent == this)
{
RemoveInternal(hoverSounds, true);
hoverSounds = null;
}
AddInternal(hoverSounds = (ActualFilename.Value == null ? new HoverClickSounds(HoverSampleSet.Button) : new HoverSounds(HoverSampleSet.Button)));
sample = ActualFilename.Value == null ? null : editorBeatmap?.BeatmapSkin?.Skin.Samples?.Get(ActualFilename.Value);
}
protected override bool OnHover(HoverEvent e)
{
Debug.Assert(triangleGradientSecondColour != null);
Background.FadeColour(triangleGradientSecondColour.Value, 300, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
Background.FadeColour(BackgroundColour, 300, Easing.OutQuint);
base.OnHoverLost(e);
}
private void addSample()
{
if (selectedFile.Value == null)
return;
this.HidePopover();
ActualFilename.Value = SampleAddRequested?.Invoke(selectedFile.Value) ?? selectedFile.Value.ToString();
}
private void deleteSample()
{
if (ActualFilename.Value == null)
return;
SampleRemoveRequested?.Invoke(ActualFilename.Value);
ActualFilename.Value = null;
}
public Popover? GetPopover() => ActualFilename.Value == null ? new FormFileSelector.FileChooserPopover(SupportedExtensions.AUDIO_EXTENSIONS, selectedFile, null) : null;
public MenuItem[]? ContextMenuItems =>
ActualFilename.Value != null
? [new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, deleteSample)]
: null;
protected override void Dispose(bool isDisposing)
{
if (editorBeatmap?.BeatmapSkin != null)
editorBeatmap.BeatmapSkin.BeatmapSkinChanged -= recycleSamples;
base.Dispose(isDisposing);
}
}
}
}

View File

@@ -186,10 +186,22 @@ namespace osu.Game.Screens.Edit.Components
}
else
{
// 设置默认的循环范围为当前时间前后2.5秒
double currentTime = editorClock.CurrentTime;
editorClock.SetLoopStartTime(editorClock.GetSnappedTime(Math.Max(0, currentTime - 2500)));
editorClock.SetLoopEndTime(editorClock.GetSnappedTime(Math.Min(editorClock.TrackLength, currentTime + 2500)));
// 默认范围:以当前活动光标为 A 起点,向后 8 个 1/4 节拍为 B 终点。
// 8 * (1/4 beat) = 2 beats.
double currentTime = Math.Clamp(editorClock.CurrentTime, 0, editorClock.TrackLength);
double startTime = editorClock.GetSnappedTime(currentTime);
var timingPoint = editorClock.ControlPointInfo.TimingPointAt(startTime);
double endTime = startTime + timingPoint.BeatLength * 2;
endTime = Math.Min(endTime, editorClock.TrackLength);
endTime = editorClock.GetSnappedTime(endTime);
if (endTime <= startTime)
endTime = Math.Min(editorClock.TrackLength, startTime + 1);
editorClock.SetLoopStartTime(startTime);
editorClock.SetLoopEndTime(endTime);
}
editorClock.Seek(editorClock.LoopStartTime.Value); // 跳转到开头

View File

@@ -1329,8 +1329,9 @@ namespace osu.Game.Screens.Edit
{
var exportItems = new List<MenuItem>
{
new EditorMenuItem(EditorStrings.ExportForEditing, MenuItemType.Standard, () => exportBeatmap(false)),
new EditorMenuItem(EditorStrings.ExportForCompatibility, MenuItemType.Standard, () => exportBeatmap(true)),
new EditorMenuItem(EditorStrings.ExportForEditing, MenuItemType.Standard, () => runExport(manager => manager.Export(Beatmap.Value.BeatmapSetInfo))),
new EditorMenuItem(EditorStrings.ExportForCompatibility, MenuItemType.Standard, () => runExport(manager => manager.ExportLegacy(Beatmap.Value.BeatmapSetInfo))),
new EditorMenuItem(EditorStrings.ExportGuestDifficulty, MenuItemType.Standard, () => runExport(manager => manager.ExportLegacy(Beatmap.Value.BeatmapInfo))),
};
return new EditorMenuItem(CommonStrings.Export) { Items = exportItems };
@@ -1396,7 +1397,7 @@ namespace osu.Game.Screens.Edit
void startSubmission() => this.Push(new BeatmapSubmissionScreen());
}
private void exportBeatmap(bool legacy)
private void runExport(Func<BeatmapManager, Task> exportAction)
{
if (HasUnsavedChanges)
{
@@ -1405,20 +1406,12 @@ namespace osu.Game.Screens.Edit
if (!Save())
return Task.CompletedTask;
return runExport();
return exportAction.Invoke(beatmapManager);
})));
}
else
{
attemptAsyncMutationOperation(runExport);
}
Task runExport()
{
if (legacy)
return beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo);
else
return beatmapManager.Export(Beatmap.Value.BeatmapSetInfo);
attemptAsyncMutationOperation(() => exportAction(beatmapManager));
}
}

View File

@@ -71,6 +71,14 @@ namespace osu.Game.Screens.Edit
}
public override string ToString() => Name;
public HashSet<string> Filenames = [];
public string? FindSampleIfExists(string sampleName, string bankName)
=> Filenames.SingleOrDefault(f => f.StartsWith($@"{bankName}-{sampleName}{(SampleSetIndex > 1 ? SampleSetIndex : null)}", StringComparison.Ordinal));
public virtual bool Equals(SampleSet? other) => SampleSetIndex == other?.SampleSetIndex;
public override int GetHashCode() => SampleSetIndex;
}
public IEnumerable<SampleSet> GetAvailableSampleSets()

View File

@@ -489,7 +489,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
if (!screen.IsCurrentScreen())
return;
var beatmap = beatmaps.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", item.Beatmap.OnlineID);
var beatmap = beatmaps.QueryOnlineBeatmapId(item.Beatmap.OnlineID);
screen.Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally.
screen.Ruleset.Value = rulesets.GetRuleset(item.RulesetID);

View File

@@ -246,7 +246,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
// Update global gameplay state to correspond to the new selection.
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", item.BeatmapID);
var localBeatmap = beatmapManager.QueryOnlineBeatmapId(item.BeatmapID);
if (localBeatmap != null)
{

View File

@@ -510,7 +510,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem;
var newBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", item.BeatmapID);
var newBeatmap = beatmapManager.QueryOnlineBeatmapId(item.BeatmapID);
if (!Beatmap.Value.BeatmapSetInfo.Equals(newBeatmap?.BeatmapSet))
this.MakeCurrent();
@@ -652,7 +652,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
// Update global gameplay state to correspond to the new selection.
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", gameplayBeatmapId);
var localBeatmap = beatmapManager.QueryOnlineBeatmapId(gameplayBeatmapId);
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
Ruleset.Value = ruleset;
Mods.Value = client.LocalUser.Mods.Concat(item.RequiredMods).Select(m => m.ToMod(rulesetInstance)).ToArray();

View File

@@ -17,7 +17,6 @@ using osu.Game.Database;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using Realms;
namespace osu.Game.Screens.OnlinePlay
{
@@ -154,7 +153,8 @@ namespace osu.Game.Screens.OnlinePlay
}
IQueryable<BeatmapInfo> queryBeatmap() =>
realm.Realm.All<BeatmapInfo>().Filter("OnlineID == $0 && MD5Hash == $1 && BeatmapSet.DeletePending == false", beatmap.OnlineID, beatmap.MD5Hash);
realm.Realm.All<BeatmapInfo>()
.ForOnlineId(beatmap.OnlineID);
}
protected override void Dispose(bool isDisposing)

View File

@@ -62,8 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[BackgroundDependencyLoader]
private void load()
{
var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}",
PlaylistItem.Beatmap.OnlineID);
var localBeatmap = beatmapManager.QueryOnlineBeatmapId(PlaylistItem.Beatmap.OnlineID);
itemBeatmap = beatmapManager.GetWorkingBeatmap(localBeatmap);
AddInternal(new Container

View File

@@ -609,7 +609,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
// Update global gameplay state to correspond to the new selection.
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", gameplayBeatmap.OnlineID);
var localBeatmap = beatmapManager.QueryOnlineBeatmapId(gameplayBeatmap.OnlineID);
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
Ruleset.Value = gameplayRuleset;
Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray();

View File

@@ -56,10 +56,10 @@ namespace osu.Game.Screens.SelectV2
{
bool match = criteria.Ruleset == null || beatmap.AllowGameplayWithRuleset(criteria.Ruleset!, criteria.AllowConvertedBeatmaps);
if (beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true)
if (criteria.SelectedBeatmapSet != null)
{
// only check ruleset equality or convertability for selected beatmap
return match;
return beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true && match;
}
if (!match) return false;

View File

@@ -0,0 +1,124 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Overlays;
namespace osu.Game.Screens.SelectV2
{
public partial class FilterControl
{
public partial class ScopedBeatmapSetDisplay : CompositeDrawable, IKeyBindingHandler<GlobalAction>
{
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet
{
get => scopedBeatmapSet.Current;
set => scopedBeatmapSet.Current = value;
}
private readonly BindableWithCurrent<BeatmapSetInfo?> scopedBeatmapSet = new BindableWithCurrent<BeatmapSetInfo?>();
private Container content = null!;
private OsuTextFlowContainer text = null!;
private ShearedButton goBackButton = null!;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
AutoSizeEasing = Easing.OutQuint;
AutoSizeDuration = 200;
CornerRadius = 8f;
Masking = true;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Highlight1,
},
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
BypassAutoSizeAxes = Axes.Y,
Shear = -OsuGame.SHEAR,
Padding = new MarginPadding
{
Horizontal = 6,
Vertical = 2,
},
Children = new Drawable[]
{
text = new OsuTextFlowContainer(t => t.Font = OsuFont.Style.Body)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Colour = colourProvider.Background6,
Padding = new MarginPadding { Right = 80, Vertical = 5 }
},
goBackButton = new ShearedButton(80)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Text = CommonStrings.Back,
RelativeSizeAxes = Axes.Y,
Height = 1,
Action = () => scopedBeatmapSet.Value = null,
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
scopedBeatmapSet.BindValueChanged(_ => updateState(), true);
}
private void updateState()
{
content.BypassAutoSizeAxes = scopedBeatmapSet.Value != null ? Axes.None : Axes.Y;
if (scopedBeatmapSet.Value != null)
{
text.Clear();
text.AddText(SongSelectStrings.TemporarilyShowingAllBeatmapsIn);
text.AddText(@" ");
text.AddText(scopedBeatmapSet.Value.Metadata.GetDisplayTitleRomanisable(), t => t.Font = OsuFont.Style.Body.With(weight: FontWeight.Bold));
}
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (scopedBeatmapSet.Value != null && e.Action == GlobalAction.Back && !e.Repeat)
{
goBackButton.TriggerClick();
return true;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
}
}
}

View File

@@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Configuration;
using osu.Game.Database;
@@ -38,6 +39,8 @@ namespace osu.Game.Screens.SelectV2
private const float corner_radius = 10;
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } = new Bindable<BeatmapSetInfo?>();
private SongSelectSearchTextBox searchTextBox = null!;
private ShearedToggleButton showConvertedBeatmapsButton = null!;
private DifficultyRangeSlider difficultyRangeSlider = null!;
@@ -160,6 +163,7 @@ namespace osu.Game.Screens.SelectV2
new Dimension(maxSize: 180),
new Dimension(GridSizeMode.Absolute, 5),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
@@ -184,6 +188,11 @@ namespace osu.Game.Screens.SelectV2
}
}
},
new ScopedBeatmapSetDisplay
{
ScopedBeatmapSet = ScopedBeatmapSet,
Depth = float.MinValue, // hack to ensure that the scoped display handles `GlobalAction.Back` input before the filter control
},
csSelector = new EzKeyModeSelector
{
RelativeSizeAxes = Axes.X,

View File

@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Scoring;
@@ -43,5 +44,10 @@ namespace osu.Game.Screens.SelectV2
/// Gets relevant actionable items for beatmap context menus, based on the type of song select.
/// </summary>
IEnumerable<OsuMenuItem> GetForwardActions(BeatmapInfo beatmap);
/// <summary>
/// Set this to a non-<see langword="null"/> value in order to temporarily bypass filter and show all difficulties of the given beatmap set.
/// </summary>
Bindable<BeatmapSetInfo?> ScopedBeatmapSet { get; }
}
}

View File

@@ -0,0 +1,259 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osuTK;
namespace osu.Game.Screens.SelectV2
{
public partial class PanelBeatmapStandalone
{
public partial class SpreadDisplay : OsuAnimatedButton
{
public Bindable<BeatmapInfo?> Beatmap { get; } = new Bindable<BeatmapInfo?>();
public Bindable<StarDifficulty> StarDifficulty { get; } = new Bindable<StarDifficulty>();
private readonly Bindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
private readonly Bindable<bool> showConvertedBeatmaps = new Bindable<bool>();
private const double transition_duration = 200;
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
private FillFlowContainer preceding = null!;
public Circle Current { get; private set; } = null!;
private FillFlowContainer succeeding = null!;
private OsuSpriteText countText = null!;
private SpriteIcon icon = null!;
public SpreadDisplay()
{
AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
Content.CornerRadius = 5;
}
[BackgroundDependencyLoader]
private void load(ISongSelect? songSelect, OsuConfigManager configManager)
{
Add(new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Padding = new MarginPadding { Horizontal = 5 },
Children = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(2),
Children = new Drawable[]
{
preceding = new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(1),
Alpha = 0.5f,
},
Current = new Circle
{
Size = new Vector2(7, 12),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
succeeding = new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(1),
Alpha = 0.5f,
}
}
},
countText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.Style.Caption2,
},
icon = new SpriteIcon
{
Size = new Vector2(12),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.Eye,
Alpha = 0,
}
}
});
if (songSelect != null)
scopedBeatmapSet.BindTo(songSelect.ScopedBeatmapSet);
configManager.BindWith(OsuSetting.ShowConvertedBeatmaps, showConvertedBeatmaps);
}
protected override void LoadComplete()
{
base.LoadComplete();
Beatmap.BindValueChanged(_ => updateBeatmap());
StarDifficulty.BindValueChanged(_ => updateBeatmap());
showConvertedBeatmaps.BindValueChanged(_ => updateBeatmap());
scopedBeatmapSet.BindValueChanged(_ => updateBeatmap(), true);
Enabled.BindValueChanged(_ => updateAppearance(), true);
FinishTransforms(true);
}
private void updateBeatmap()
{
if (Beatmap.Value == null || scopedBeatmapSet.Value != null)
{
this.FadeOut(transition_duration, Easing.OutQuint);
return;
}
preceding.Clear();
succeeding.Clear();
var otherStarDifficulties = Beatmap.Value.BeatmapSet!.Beatmaps
.Except([Beatmap.Value])
.Where(b => b.AllowGameplayWithRuleset(ruleset.Value, showConvertedBeatmaps.Value))
.OrderBy(b => b.StarRating)
.Select(b => b.StarRating)
.ToList();
this.FadeTo(otherStarDifficulties.Count > 0 ? 1 : 0, transition_duration, Easing.OutQuint);
if (otherStarDifficulties.Count == 0)
return;
const int max_difficulties_total = 11;
int startIndex;
int endIndex;
if (otherStarDifficulties.Count <= max_difficulties_total)
{
startIndex = 0;
endIndex = otherStarDifficulties.Count - 1;
}
else
{
startIndex = otherStarDifficulties.BinarySearch(StarDifficulty.Value.Stars);
if (startIndex < 0)
startIndex = ~startIndex - 1;
startIndex = Math.Clamp(startIndex - max_difficulties_total / 2, 0, otherStarDifficulties.Count - 1);
endIndex = Math.Clamp(startIndex + max_difficulties_total, 0, otherStarDifficulties.Count - 1);
}
for (int i = startIndex; i <= endIndex; i++)
{
double otherStarDifficulty = otherStarDifficulties[i];
var target = otherStarDifficulty < StarDifficulty.Value.Stars ? preceding : succeeding;
var circle = new Circle
{
Size = new Vector2(5, 10),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Colour = colours.ForStarDifficulty(otherStarDifficulty)
};
target.Add(circle);
target.SetLayoutPosition(circle, (float)otherStarDifficulty);
}
int countNotShown = otherStarDifficulties.Count - (preceding.Count + succeeding.Count);
countText.Alpha = countNotShown > 0 ? 1 : 0;
countText.Text = $@"+{countNotShown}";
if (startIndex > 0)
{
for (int i = 0; i < preceding.Count; ++i)
{
var dot = preceding[i];
dot.Alpha = (1 + 4 * (float)(i + 1) / preceding.Count) / 5;
}
}
if (endIndex < otherStarDifficulties.Count - 1)
{
for (int i = 0; i < succeeding.Count; ++i)
{
var dot = succeeding[i];
dot.Alpha = (1 + 4 * (float)(succeeding.Count - i) / succeeding.Count) / 5;
}
}
Action = () => scopedBeatmapSet.Value = Beatmap.Value.BeatmapSet!;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (!Enabled.Value)
return false;
base.OnMouseDown(e);
return true;
}
protected override bool OnClick(ClickEvent e)
{
if (!Enabled.Value)
return false;
return base.OnClick(e);
}
protected override bool OnHover(HoverEvent e)
{
updateAppearance();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateAppearance();
base.OnHoverLost(e);
}
private void updateAppearance()
{
bool isInteractable = Enabled.Value && IsHovered;
HoverColour = isInteractable ? Colour4.White.Opacity(0.1f) : Colour4.Transparent;
preceding.FadeTo(isInteractable ? 1 : 0.5f, transition_duration, Easing.OutQuint);
succeeding.FadeTo(isInteractable ? 1 : 0.5f, transition_duration, Easing.OutQuint);
icon.FadeTo(isInteractable ? 1 : 0, transition_duration, Easing.OutQuint);
}
}
}
}

View File

@@ -81,7 +81,7 @@ namespace osu.Game.Screens.SelectV2
private ConstrainedIconContainer difficultyIcon = null!;
private StarRatingDisplay starRatingDisplay = null!;
private StarCounter starCounter = null!;
private SpreadDisplay spreadDisplay = null!;
private PanelLocalRankDisplay localRank = null!;
private OsuSpriteText keyCountText = null!;
private OsuSpriteText difficultyText = null!;
@@ -210,11 +210,11 @@ namespace osu.Game.Screens.SelectV2
Anchor = Anchor.CentreLeft,
Scale = new Vector2(0.875f),
},
starCounter = new StarCounter
spreadDisplay = new SpreadDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Scale = new Vector2(0.4f)
Anchor = Anchor.CentreLeft,
Enabled = { BindTarget = Selected }
},
new OsuSpriteText
{
@@ -266,6 +266,7 @@ namespace osu.Game.Screens.SelectV2
updateCalculationsAsync(beatmap);
computeStarRating();
spreadDisplay.Beatmap.Value = beatmap;
updateKeyCount();
}
@@ -351,6 +352,7 @@ namespace osu.Game.Screens.SelectV2
updateButton.BeatmapSet = null;
localRank.Beatmap = null;
starDifficultyBindable = null;
spreadDisplay.Beatmap.Value = null;
starDifficultyCancellationSource?.Cancel();
kpsCalculationCancellationSource?.Cancel();
@@ -368,7 +370,7 @@ namespace osu.Game.Screens.SelectV2
starDifficultyBindable.BindValueChanged(starDifficulty =>
{
starRatingDisplay.Current.Value = starDifficulty.NewValue;
starCounter.Current = (float)starDifficulty.NewValue.Stars;
spreadDisplay.StarDifficulty.Value = starDifficulty.NewValue;
}, true);
}
@@ -389,7 +391,7 @@ namespace osu.Game.Screens.SelectV2
var diffColour = starRatingDisplay.DisplayedDifficultyColour;
AccentColour = diffColour;
starCounter.Colour = diffColour;
spreadDisplay.Current.Colour = diffColour;
backgroundBorder.Colour = diffColour;
difficultyIcon.Colour = starRatingDisplay.DisplayedStars.Value > OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider.Background5;

View File

@@ -1225,6 +1225,8 @@ namespace osu.Game.Screens.SelectV2
beatmaps.Restore(b);
}
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet => filterControl.ScopedBeatmapSet;
#endregion
}
}

View File

@@ -23,9 +23,15 @@ namespace osu.Game.Utils
{
}
}
}
return true;
// aside from opening every zip entry not failing, we also require there to *be* at least one entry.
// if there are no entries, the best case is that it's an actual empty zip
// and as such probably useless to whatever wants to use it later.
// the worst case is that it's actually *not* a zip and instead a stream of binary
// which *accidentally* happened to contain the magic sequence of bytes for the zip header (50 4b 05 06),
// and if that's the case, then we are *misclassifying* it as a zip by returning `true` unconditionally.
return arc.Entries.Count > 0;
}
}
catch (Exception)
{
@@ -52,9 +58,15 @@ namespace osu.Game.Utils
{
}
}
}
return true;
// aside from opening every zip entry not failing, we also require there to *be* at least one entry.
// if there are no entries, the best case is that it's an actual empty zip
// and as such probably useless to whatever wants to use it later.
// the worst case is that it's actually *not* a zip and instead a stream of binary
// which *accidentally* happened to contain the magic sequence of bytes for the zip header (50 4b 05 06),
// and if that's the case, then we are *misclassifying* it as a zip by returning `true` unconditionally.
return arc.Entries.Count > 0;
}
}
catch (Exception)
{