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

Merge branch 'master' into argon-pp-counter

This commit is contained in:
Dean Herbert 2024-03-08 10:32:16 +08:00 committed by GitHub
commit d9cc619693
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
103 changed files with 1780 additions and 532 deletions

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.223.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.306.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
<PackageReference Include="nunit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>

View File

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

View File

@ -0,0 +1,58 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Scoring;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
public class CatchHealthProcessorTest
{
private static readonly object[][] test_cases =
[
// hitobject, starting HP, fail expected after miss
[new Fruit(), 0.01, true],
[new Droplet(), 0.01, true],
[new TinyDroplet(), 0, false],
[new Banana(), 0, false],
];
[TestCaseSource(nameof(test_cases))]
public void TestFailAfterMinResult(CatchHitObject hitObject, double startingHealth, bool failExpected)
{
var healthProcessor = new CatchHealthProcessor(0);
healthProcessor.ApplyBeatmap(new CatchBeatmap
{
HitObjects = { hitObject }
});
healthProcessor.Health.Value = startingHealth;
var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MinResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
}
[TestCaseSource(nameof(test_cases))]
public void TestNoFailAfterMaxResult(CatchHitObject hitObject, double startingHealth, bool _)
{
var healthProcessor = new CatchHealthProcessor(0);
healthProcessor.ApplyBeatmap(new CatchBeatmap
{
HitObjects = { hitObject }
});
healthProcessor.Health.Value = startingHealth;
var result = new CatchJudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MaxResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.False);
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,210 @@
osu file format v14
[General]
AudioFilename: audio.mp3
AudioLeadIn: 0
PreviewTime: 65316
Countdown: 0
SampleSet: Soft
StackLeniency: 0.7
Mode: 2
LetterboxInBreaks: 0
WidescreenStoryboard: 0
[Editor]
DistanceSpacing: 1.4
BeatDivisor: 4
GridSize: 8
TimelineZoom: 1.4
[Metadata]
Title:Nanairo Symphony -TV Size-
TitleUnicode:七色シンフォニー -TV Size-
Artist:Coalamode.
ArtistUnicode:コアラモード.
Creator:Ascendance
Version:Aru's Cup
Source:四月は君の嘘
Tags:shigatsu wa kimi no uso your lie in april opening arusamour tenshichan [superstar]
BeatmapID:1041052
BeatmapSetID:488149
[Difficulty]
HPDrainRate:3
CircleSize:2.5
OverallDifficulty:6
ApproachRate:6
SliderMultiplier:1.02
SliderTickRate:2
[Events]
//Background and Video events
Video,500,"forty.avi"
0,0,"cropped-1366-768-647733.jpg",0,0
//Break Periods
//Storyboard Layer 0 (Background)
//Storyboard Layer 1 (Fail)
//Storyboard Layer 2 (Pass)
//Storyboard Layer 3 (Foreground)
//Storyboard Sound Samples
[TimingPoints]
1155,387.096774193548,4,2,1,50,1,0
15284,-100,4,2,1,60,0,0
16638,-100,4,2,1,50,0,0
41413,-100,4,2,1,60,0,0
59993,-100,4,2,1,65,0,0
66187,-100,4,2,1,70,0,1
87284,-100,4,2,1,60,0,1
87864,-100,4,2,1,70,0,0
87961,-100,4,2,1,50,0,0
88638,-100,4,2,1,30,0,0
89413,-100,4,2,1,10,0,0
89800,-100,4,2,1,5,0,0
[Colours]
Combo1 : 255,128,64
Combo2 : 0,128,255
Combo3 : 255,128,192
Combo4 : 0,128,192
[HitObjects]
208,160,1155,6,0,L|45:160,1,153,2|2,0:0|0:0,0:0:0:0:
160,160,2122,1,0,0:0:0:0:
272,160,2509,1,2,0:0:0:0:
448,288,3284,6,0,P|480:240|480:192,1,102,2|0,0:0|0:0,0:0:0:0:
384,96,4058,1,2,0:0:0:0:
128,64,5025,6,0,L|32:64,2,76.5,2|0|0,0:0|0:0|0:0,0:0:0:0:
192,64,5800,1,2,0:0:0:0:
240,64,5993,1,2,0:0:0:0:
288,64,6187,1,2,0:0:0:0:
416,80,6574,6,0,L|192:80,1,204,0|2,0:0|0:0,0:0:0:0:
488,160,8122,2,0,L|376:160,1,102
457,288,8896,2,0,L|297:288,1,153,2|2,0:0|0:0,0:0:0:0:
400,288,10058,1,0,0:0:0:0:
304,288,10445,6,0,L|192:288,2,102,2|0|2,0:0|0:0|0:0,0:0:0:0:
400,288,11606,1,0,0:0:0:0:
240,288,11993,2,0,L|80:288,1,153,2|0,0:0|0:0,0:0:0:0:
0,288,13154,1,0,0:0:0:0:
112,240,13542,6,0,P|160:288|256:288,1,153,6|2,0:0|0:0,0:0:0:0:
288,288,14316,2,0,L|368:288,2,76.5,2|0|0,0:0|0:0|0:0,0:0:0:0:
192,288,15284,2,0,L|160:224,1,51,0|12,0:0|0:0,0:0:0:0:
312,208,15864,1,6,0:0:0:0:
128,176,16638,6,0,P|64:160|0:96,2,153,6|2|0,0:0|0:0|0:0,0:0:0:0:
224,176,18187,2,0,P|288:192|352:272,2,153,2|2|0,0:0|0:0|0:0,0:0:0:0:
128,176,19735,6,0,L|288:176,1,153,2|2,0:0|0:0,0:0:0:0:
432,176,20896,1,0,0:0:0:0:
328,176,21284,2,0,L|488:176,1,153,2|2,0:0|0:0,0:0:0:0:
328,176,22445,1,0,0:0:0:0:
224,176,22832,6,0,L|64:176,1,153,2|2,0:0|0:0,0:0:0:0:
224,176,23993,1,0,0:0:0:0:
112,176,24380,2,0,L|272:176,1,153,2|2,0:0|0:0,0:0:0:0:
416,176,25541,1,0,0:0:0:0:
304,256,25929,6,0,P|272:208|312:120,1,153,2|2,0:0|0:0,0:0:0:0:
480,112,27090,1,0,0:0:0:0:
384,112,27477,6,0,L|320:112,2,51,2|2|0,0:0|0:0|0:0,0:0:0:0:
432,112,28058,1,2,0:0:0:0:
333,112,28445,2,0,L|282:112,2,51,0|0|0,0:0|0:0|0:0,0:0:0:0:
384,112,29025,6,0,L|272:112,1,102,6|0,0:0|0:0,0:0:0:0:
224,112,29606,2,0,P|160:144|160:240,1,153,2|2,0:0|0:0,0:0:0:0:
272,272,30574,2,0,L|374:272,1,102
424,272,31154,2,0,P|414:344|348:378,1,153,0|0,0:0|0:0,0:0:0:0:
224,304,32122,6,0,P|176:320|144:368,1,102,2|0,0:0|0:0,0:0:0:0:
200,368,32703,1,2,0:0:0:0:
376,368,33284,1,0,0:0:0:0:
304,296,33671,2,0,L|240:296,2,51,2|2|0,0:0|0:0|0:0,0:0:0:0:
352,296,34251,2,0,P|400:248|384:168,1,153,2|0,0:0|0:0,0:0:0:0:
280,176,35219,6,0,L|216:80,1,102,2|0,0:0|0:0,0:0:0:0:
272,104,35800,2,0,L|336:8,1,102,2|0,0:0|0:0,0:0:0:0:
280,16,36380,1,2,0:0:0:0:
176,32,36767,6,0,L|112:128,1,102,2|0,0:0|0:0,0:0:0:0:
168,128,37348,2,0,L|232:224,1,102,2|0,0:0|0:0,0:0:0:0:
176,224,37928,1,2,0:0:0:0:
304,264,38316,6,0,L|200:264,1,102,2|0,0:0|0:0,0:0:0:0:
144,264,38896,1,2,0:0:0:0:
280,336,39477,2,0,L|336:336,1,51
424,336,39864,2,0,P|440:304|416:240,1,102,8|0,0:3|0:3,0:3:0:0:
352,232,40445,1,4,0:1:0:0:
160,224,41025,1,8,0:3:0:0:
256,48,41413,6,0,P|302:28|353:31,1,102,6|0,0:0|0:0,0:0:0:0:
400,40,41993,1,0,0:0:0:0:
440,80,42187,2,0,P|389:76|342:96,1,102,2|8,0:0|0:0,0:0:0:0:
248,128,42961,2,0,P|312:176|392:144,2,153,2|2|8,0:0|0:0|0:3,0:0:0:0:
144,136,44509,6,0,P|80:88|0:120,1,153,2|0,0:0|0:0,0:0:0:0:
56,136,45284,1,2,0:0:0:0:
160,144,45671,1,8,0:0:0:0:
264,144,46058,2,0,L|384:144,1,102,2|0,0:0|0:0,0:0:0:0:
416,152,46638,2,0,L|264:152,1,153,2|8,0:0|0:3,0:0:0:0:
360,120,47606,6,0,L|192:120,1,153,2|0,0:0|0:0,0:0:0:0:
160,128,48380,2,0,P|208:80|256:96,1,102,2|8,0:0|0:0,0:0:0:0:
144,136,49154,1,2,0:0:0:0:
248,144,49542,2,0,L|368:144,1,102,0|2,0:0|0:0,0:0:0:0:
256,192,50316,2,0,L|200:192,1,51,10|0,0:0|0:0,0:0:0:0:
256,184,50703,6,0,L|360:184,1,102,2|0,0:0|0:0,0:0:0:0:
400,208,51284,1,0,0:0:0:0:
352,240,51477,2,0,L|240:240,1,102
128,336,52251,6,0,P|64:336|0:256,1,153,2|2,0:0|0:0,0:0:0:0:
88,264,53025,1,2,0:0:0:0:
168,208,53413,2,0,L|152:144,1,51,8|8,0:0|0:3,0:0:0:0:
248,120,53800,6,0,P|328:152|392:120,1,153,6|0,0:0|0:0,0:0:0:0:
432,120,54574,1,2,0:0:0:0:
328,128,54961,1,8,0:0:0:0:
224,128,55348,6,0,L|112:144,1,102,2|0,0:0|0:0,0:0:0:0:
72,152,55929,2,0,L|192:176,1,102,2|0,0:0|0:0,0:0:0:0:
224,184,56509,1,8,0:3:0:0:
328,176,56896,6,0,P|376:208|472:192,1,153,2|0,0:0|0:0,0:0:0:0:
416,208,57671,2,0,L|304:240,1,102,2|8,0:0|0:0,0:0:0:0:
224,272,58445,5,2,0:0:0:0:
320,296,58832,1,0,0:0:0:0:
224,328,59219,1,2,0:0:0:0:
120,328,59606,1,8,0:3:0:0:
224,264,59993,6,0,P|224:200|192:152,1,102,6|0,0:0|0:0,0:0:0:0:
80,184,60767,2,0,P|76:133|97:87,1,102,2|8,0:0|0:0,0:0:0:0:
200,80,61542,2,0,P|232:112|296:112,1,102,2|0,0:0|0:0,0:0:0:0:
376,160,62316,2,0,P|344:192|280:192,1,102,2|8,0:0|0:0,0:0:0:0:
184,240,63090,6,0,L|200:128,1,102,2|8,0:0|0:0,0:0:0:0:
88,136,63864,2,0,L|8:152,2,76.5,6|2|2,0:0|0:0|0:0,0:0:0:0:
160,112,64638,1,8,0:0:0:0:
208,128,64832,1,8,0:0:0:0:
256,144,65025,1,8,0:0:0:0:
360,152,65413,6,0,L|424:152,1,51,8|0,0:0|0:0,0:0:0:0:
462,152,65800,2,0,L|398:152,1,51,8|8,0:0|0:3,0:0:0:0:
344,144,66187,6,0,L|232:144,1,102,12|8,0:0|0:0,0:0:0:0:
152,120,66961,2,0,P|148:169|107:196,1,102,8|8,0:0|0:0,0:0:0:0:
32,264,67735,6,0,L|144:216,1,102,8|8,0:0|0:0,0:0:0:0:
176,208,68316,1,0,0:0:0:0:
224,200,68509,2,0,L|317:240,1,102,8|8,0:0|0:0,0:0:0:0:
216,256,69284,6,0,P|184:304|200:352,1,102,8|8,0:0|0:0,0:0:0:0:
360,256,70058,2,0,P|368:207|337:167,1,102,8|8,0:0|0:0,0:0:0:0:
264,80,70832,6,0,L|152:96,1,102,8|8,0:0|0:0,0:0:0:0:
112,104,71413,2,0,L|11:89,1,102,8|0,0:0|0:0,0:0:0:0:
40,128,71993,2,0,L|72:176,1,51,8|8,0:0|0:3,0:0:0:0:
176,216,72380,6,0,P|144:280|64:280,1,153,12|0,0:0|0:0,0:0:0:0:
120,280,73154,2,0,P|191:299|216:328,1,102,8|8,0:0|0:0,0:0:0:0:
312,320,73929,6,0,L|424:304,1,102,8|8,0:0|0:0,0:0:0:0:
336,272,74703,2,0,L|312:216,1,51,8|0,0:0|0:0,0:0:0:0:
400,200,75090,2,0,L|424:136,1,51,8|0,0:0|0:0,0:0:0:0:
328,152,75477,6,0,P|280:184|200:136,1,153,12|0,0:0|0:0,0:0:0:0:
296,136,76251,2,0,P|360:136|408:168,1,102,8|8,0:0|0:0,0:0:0:0:
152,248,77219,6,0,L|96:248,2,51,0|12|0,0:0|0:0|0:0,0:0:0:0:
208,248,77800,1,8,0:0:0:0:
320,256,78187,2,0,L|369:243,1,51,8|8,0:0|0:3,0:0:0:0:
456,232,78574,6,0,L|408:136,1,102,12|8,0:0|0:0,0:0:0:0:
288,136,79348,2,0,L|336:40,1,102,8|8,0:0|0:0,0:0:0:0:
240,80,80122,6,0,P|144:80|128:64,1,102,8|8,0:0|0:0,0:0:0:0:
96,72,80703,1,0,0:0:0:0:
40,104,80896,2,0,P|136:104|152:88,1,102,8|8,0:0|0:0,0:0:0:0:
248,128,81671,6,0,L|296:224,1,102,12|8,0:0|0:0,0:0:0:0:
208,272,82445,1,10,0:0:0:0:
312,272,82832,1,8,0:0:0:0:
400,224,83219,6,0,L|416:160,1,51,8|2,0:0|0:0,0:0:0:0:
360,56,83606,2,0,L|336:120,1,51,8|0,0:0|0:0,0:0:0:0:
272,152,83993,2,0,P|192:152|176:136,1,102,0|8,0:0|0:0,0:0:0:0:
80,160,84767,6,0,L|96:208,1,51,8|0,0:0|0:0,0:0:0:0:
16,272,85154,2,0,L|16:328,1,51,8|0,0:0|0:0,0:0:0:0:
104,304,85542,2,0,L|208:304,1,102,2|8,0:0|0:0,0:0:0:0:
376,336,86316,6,0,L|472:304,1,102,4|0,0:0|0:0,0:0:0:0:
296,248,87090,2,0,P|312:168|312:136,1,102,2|8,0:0|0:3,0:0:0:0:
168,96,87864,1,4,0:0:0:0:
256,192,88251,12,0,89800,0:0:0:0:

View File

@ -118,7 +118,11 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
float offsetPosition = hitObject.OriginalX;
double startTime = hitObject.StartTime;
if (lastPosition == null)
if (lastPosition == null ||
// some objects can get assigned position zero, making stable incorrectly go inside this if branch on the next object. to maintain behaviour and compatibility, do the same here.
// reference: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameplayElements/HitObjects/Fruits/HitFactoryFruits.cs#L45-L50
// todo: should be revisited and corrected later probably.
lastPosition == 0)
{
lastPosition = offsetPosition;
lastStartTime = startTime;

View File

@ -2,22 +2,21 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchPerformanceCalculator : PerformanceCalculator
{
private int fruitsHit;
private int ticksHit;
private int tinyTicksHit;
private int tinyTicksMissed;
private int misses;
private int num300;
private int num100;
private int num50;
private int numKatu;
private int numMiss;
public CatchPerformanceCalculator()
: base(new CatchRuleset())
@ -28,11 +27,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
var catchAttributes = (CatchDifficultyAttributes)attributes;
fruitsHit = score.Statistics.GetValueOrDefault(HitResult.Great);
ticksHit = score.Statistics.GetValueOrDefault(HitResult.LargeTickHit);
tinyTicksHit = score.Statistics.GetValueOrDefault(HitResult.SmallTickHit);
tinyTicksMissed = score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
misses = score.Statistics.GetValueOrDefault(HitResult.Miss);
num300 = score.GetCount300() ?? 0; // HitResult.Great
num100 = score.GetCount100() ?? 0; // HitResult.LargeTickHit
num50 = score.GetCount50() ?? 0; // HitResult.SmallTickHit
numKatu = score.GetCountKatu() ?? 0; // HitResult.SmallTickMiss
numMiss = score.GetCountMiss() ?? 0; // HitResult.Miss PLUS HitResult.LargeTickMiss
// We are heavily relying on aim in catch the beat
double value = Math.Pow(5.0 * Math.Max(1.0, catchAttributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
(numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0);
value *= lengthBonus;
value *= Math.Pow(0.97, misses);
value *= Math.Pow(0.97, numMiss);
// Combo scaling
if (catchAttributes.MaxCombo > 0)
@ -86,8 +85,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
}
private double accuracy() => totalHits() == 0 ? 0 : Math.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1);
private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed;
private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit;
private int totalComboHits() => misses + ticksHit + fruitsHit;
private int totalHits() => num50 + num100 + num300 + numMiss + numKatu;
private int totalSuccessfulHits() => num50 + num100 + num300;
private int totalComboHits() => numMiss + num100 + num300;
}
}

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
@ -21,6 +22,19 @@ namespace osu.Game.Rulesets.Catch.Scoring
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty<HitObject>();
protected override bool CheckDefaultFailCondition(JudgementResult result)
{
// matches stable.
// see: https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L967
// the above early-return skips the failure check at the end of the same method:
// https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Play/Rulesets/Ruleset.cs#L1232
// making it impossible to fail on a tiny droplet regardless of result.
if (result.Type == HitResult.SmallTickMiss)
return false;
return base.CheckDefaultFailCondition(result);
}
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
{
double increase = 0;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
@ -27,5 +28,49 @@ namespace osu.Game.Rulesets.Mania.Tests
// No matter what, mania doesn't have passive HP drain.
Assert.That(processor.DrainRate, Is.Zero);
}
private static readonly object[][] test_cases =
[
// hitobject, starting HP, fail expected after miss
[new Note(), 0.01, true],
[new HeadNote(), 0.01, true],
[new TailNote(), 0.01, true],
[new HoldNoteBody(), 0, true], // hold note break
[new HoldNote(), 0, true],
];
[TestCaseSource(nameof(test_cases))]
public void TestFailAfterMinResult(ManiaHitObject hitObject, double startingHealth, bool failExpected)
{
var healthProcessor = new ManiaHealthProcessor(0);
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
{
HitObjects = { hitObject }
});
healthProcessor.Health.Value = startingHealth;
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MinResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
}
[TestCaseSource(nameof(test_cases))]
public void TestNoFailAfterMaxResult(ManiaHitObject hitObject, double startingHealth, bool _)
{
var healthProcessor = new ManiaHealthProcessor(0);
healthProcessor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
{
HitObjects = { hitObject }
});
healthProcessor.Health.Value = startingHealth;
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MaxResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.False);
}
}
}

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.UI;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Mods;
@ -101,6 +102,14 @@ namespace osu.Game.Rulesets.Mania.Mods
return base.GetHeight(coverage) * reference_playfield_height / availablePlayfieldHeight;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (skin.IsNotNull())
skin.SourceChanged -= onSkinChanged;
}
}
}
}

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.UI;
@ -46,17 +47,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
// Key images are placed side-to-side on the playfield, therefore ClampToEdge must be used to prevent any gaps between each key.
upSprite = new Sprite
{
Origin = Anchor.BottomCentre,
Texture = skin.GetTexture(upImage),
Texture = skin.GetTexture(upImage, WrapMode.ClampToEdge, default),
RelativeSizeAxes = Axes.X,
Width = 1
},
downSprite = new Sprite
{
Origin = Anchor.BottomCentre,
Texture = skin.GetTexture(downImage),
Texture = skin.GetTexture(downImage, WrapMode.ClampToEdge, default),
RelativeSizeAxes = Axes.X,
Width = 1,
Alpha = 0

View File

@ -0,0 +1,66 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class OsuHealthProcessorTest
{
private static readonly object[][] test_cases =
[
// hitobject, starting HP, fail expected after miss
[new HitCircle(), 0.01, true],
[new SliderHeadCircle(), 0.01, true],
[new SliderHeadCircle { ClassicSliderBehaviour = true }, 0.01, true],
[new SliderTick(), 0.01, true],
[new SliderRepeat(new Slider()), 0.01, true],
[new SliderTailCircle(new Slider()), 0, true],
[new SliderTailCircle(new Slider()) { ClassicSliderBehaviour = true }, 0.01, true],
[new Slider(), 0, true],
[new Slider { ClassicSliderBehaviour = true }, 0.01, true],
[new SpinnerTick(), 0, false],
[new SpinnerBonusTick(), 0, false],
[new Spinner(), 0.01, true],
];
[TestCaseSource(nameof(test_cases))]
public void TestFailAfterMinResult(OsuHitObject hitObject, double startingHealth, bool failExpected)
{
var healthProcessor = new OsuHealthProcessor(0);
healthProcessor.ApplyBeatmap(new OsuBeatmap
{
HitObjects = { hitObject }
});
healthProcessor.Health.Value = startingHealth;
var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MinResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
}
[TestCaseSource(nameof(test_cases))]
public void TestNoFailAfterMaxResult(OsuHitObject hitObject, double startingHealth, bool _)
{
var healthProcessor = new OsuHealthProcessor(0);
healthProcessor.ApplyBeatmap(new OsuBeatmap
{
HitObjects = { hitObject }
});
healthProcessor.Health.Value = startingHealth;
var result = new OsuJudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MaxResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.False);
}
}
}

View File

@ -457,6 +457,33 @@ namespace osu.Game.Rulesets.Osu.Tests
assertMidSliderJudgementFail();
}
[Test]
public void TestRewindHandling()
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(0), Actions = { OsuAction.LeftButton }, Time = time_slider_start },
new OsuReplayFrame { Position = new Vector2(175, 0), Actions = { OsuAction.LeftButton }, Time = 3250 },
new OsuReplayFrame { Position = new Vector2(175, 0), Actions = { OsuAction.LeftButton }, Time = time_slider_end },
}, new Slider
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
{
Vector2.Zero,
new Vector2(250, 0),
}, 250),
});
AddUntilStep("wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
AddAssert("no miss judgements recorded", () => judgementResults.All(r => r.Type.IsHit()));
AddStep("rewind to middle of slider", () => currentPlayer.Seek(time_during_slide_4));
AddUntilStep("wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
AddAssert("no miss judgements recorded", () => judgementResults.All(r => r.Type.IsHit()));
}
private void assertAllMaxJudgements()
{
AddAssert("All judgements max", () =>

View File

@ -58,9 +58,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
private void applyStacking(Beatmap<OsuHitObject> beatmap, int startIndex, int endIndex)
{
if (startIndex > endIndex) throw new ArgumentOutOfRangeException(nameof(startIndex), $"{nameof(startIndex)} cannot be greater than {nameof(endIndex)}.");
if (startIndex < 0) throw new ArgumentOutOfRangeException(nameof(startIndex), $"{nameof(startIndex)} cannot be less than 0.");
if (endIndex < 0) throw new ArgumentOutOfRangeException(nameof(endIndex), $"{nameof(endIndex)} cannot be less than 0.");
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
ArgumentOutOfRangeException.ThrowIfNegative(endIndex);
int extendedEndIndex = endIndex;

View File

@ -334,7 +334,7 @@ namespace osu.Game.Rulesets.Osu.Edit
/// <returns>The <see cref="OsuDistanceSnapGrid"/> from a selected <see cref="HitObject"/> to a target <see cref="HitObject"/>.</returns>
private OsuDistanceSnapGrid createGrid(Func<HitObject, bool> sourceSelector, int targetOffset = 1)
{
if (targetOffset < 1) throw new ArgumentOutOfRangeException(nameof(targetOffset));
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset);
int sourceIndex = -1;

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Osu.Judgements
{
public class OsuSliderJudgementResult : OsuJudgementResult
{
public readonly Stack<(double time, bool tracking)> TrackingHistory = new Stack<(double, bool)>();
public OsuSliderJudgementResult(HitObject hitObject, Judgement judgement)
: base(hitObject, judgement)
{
TrackingHistory.Push((double.NegativeInfinity, false));
}
}
}

View File

@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Mods
// multiply the SPM by 1.01 to ensure that the spinner is completed. if the calculation is left exact,
// some spinners may not complete due to very minor decimal loss during calculation
float rotationSpeed = (float)(1.01 * spinner.HitObject.SpinsRequired / spinner.HitObject.Duration);
spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f));
spinner.RotationTracker.AddRotation(float.RadiansToDegrees((float)rateIndependentElapsedTime * rotationSpeed * MathF.PI * 2.0f));
}
}
}

View File

@ -14,8 +14,10 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Layout;
using osu.Game.Audio;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
@ -27,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public new Slider HitObject => (Slider)base.HitObject;
public new OsuSliderJudgementResult Result => (OsuSliderJudgementResult)base.Result;
public DrawableSliderHead HeadCircle => headContainer.Child;
public DrawableSliderTail TailCircle => tailContainer.Child;
@ -134,6 +138,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}, true);
}
protected override JudgementResult CreateResult(Judgement judgement) => new OsuSliderJudgementResult(HitObject, judgement);
protected override void OnApply()
{
base.OnApply();

View File

@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break;
}
float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
float aimRotation = float.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
while (Math.Abs(aimRotation - Arrow.Rotation) > 180)
aimRotation += aimRotation < Arrow.Rotation ? 360 : -360;

View File

@ -5,11 +5,14 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Play;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
@ -21,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary>
public bool Tracking { get; private set; }
[Resolved]
private IGameplayClock? gameplayClock { get; set; }
/// <summary>
/// The point in time after which we can accept any key for tracking. Before this time, we may need to restrict tracking to the key used to hit the head circle.
///
@ -49,6 +55,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public SliderInputManager(DrawableSlider slider)
{
this.slider = slider;
this.slider.HitObjectApplied += resetState;
}
/// <summary>
@ -208,6 +215,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// <param name="isValidTrackingPosition">Whether the current mouse position is valid to begin tracking.</param>
private void updateTracking(bool isValidTrackingPosition)
{
if (gameplayClock?.IsRewinding == true)
{
var trackingHistory = slider.Result.TrackingHistory;
while (trackingHistory.TryPeek(out var historyEntry) && Time.Current < historyEntry.time)
trackingHistory.Pop();
Debug.Assert(trackingHistory.Count > 0);
Tracking = trackingHistory.Peek().tracking;
return;
}
bool wasTracking = Tracking;
// from the point at which the head circle is hit, this will be non-null.
// it may be null if the head circle was missed.
OsuAction? headCircleHitAction = getInitialHitAction();
@ -247,6 +268,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
&& isValidTrackingPosition
// valid action
&& validTrackingAction;
if (wasTracking != Tracking)
slider.Result.TrackingHistory.Push((Time.Current, Tracking));
}
private OsuAction? getInitialHitAction() => slider.HeadCircle?.HitAction;
@ -264,5 +288,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return action == OsuAction.LeftButton || action == OsuAction.RightButton;
}
private void resetState(DrawableHitObject obj)
{
Tracking = false;
timeToAcceptAnyKeyAfter = null;
lastPressedActions.Clear();
screenSpaceMousePosition = null;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
slider.HitObjectApplied -= resetState;
}
}
}

View File

@ -342,7 +342,7 @@ namespace osu.Game.Rulesets.Osu.Replays
// 0.05 rad/ms, or ~477 RPM, as per stable.
// the redundant conversion from RPM to rad/ms is here for ease of testing custom SPM specs.
const float spin_rpm = 0.05f / (2 * MathF.PI) * 60000;
float radsPerMillisecond = MathUtils.DegreesToRadians(spin_rpm * 360) / 60000;
float radsPerMillisecond = float.DegreesToRadians(spin_rpm * 360) / 60000;
switch (h)
{

View File

@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
Origin = Anchor.Centre,
Colour = Color4.White.Opacity(0.25f),
RelativeSizeAxes = Axes.Both,
Current = { Value = arc_fill },
Progress = arc_fill,
Rotation = 90 - arc_fill * 180,
InnerRadius = arc_radius,
RoundedCaps = true,
@ -71,9 +71,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
background.Alpha = spinner.Progress >= 1 ? 0 : 1;
fill.Alpha = (float)Interpolation.DampContinuously(fill.Alpha, spinner.Progress > 0 && spinner.Progress < 1 ? 1 : 0, 40f, (float)Math.Abs(Time.Elapsed));
fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? 0 : arc_fill * spinner.Progress, 40f, (float)Math.Abs(Time.Elapsed));
fill.Progress = (float)Interpolation.DampContinuously(fill.Progress, spinner.Progress >= 1 ? 0 : arc_fill * spinner.Progress, 40f, (float)Math.Abs(Time.Elapsed));
fill.Rotation = (float)(90 - fill.Current.Value * 180);
fill.Rotation = (float)(90 - fill.Progress * 180);
}
private partial class ProgressFill : CircularProgress

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Current = { Value = arc_fill },
Progress = arc_fill,
Rotation = -arc_fill * 180,
InnerRadius = arc_radius,
RoundedCaps = true,
@ -44,10 +44,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
base.Update();
fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed));
fill.Progress = (float)Interpolation.DampContinuously(fill.Progress, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed));
fill.InnerRadius = (float)Interpolation.DampContinuously(fill.InnerRadius, spinner.Progress >= 1 ? arc_radius * 2.2f : arc_radius, 40f, (float)Math.Abs(Time.Elapsed));
fill.Rotation = (float)(-fill.Current.Value * 180);
fill.Rotation = (float)(-fill.Progress * 180);
}
}
}

View File

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
if (mousePosition is Vector2 pos)
{
float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2));
float thisAngle = -float.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2));
float delta = lastAngle == null ? 0 : thisAngle - lastAngle.Value;
// Normalise the delta to -180 .. 180

View File

@ -246,7 +246,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
// Likewise sin(pi/2)=1 and sin(3pi/2)=-1, whereas we actually want these values to appear on the bottom/top respectively, so the y-coordinate also needs to be inverted.
//
// We also need to apply the anti-clockwise rotation.
double rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation);
double rotatedAngle = finalAngle - float.DegreesToRadians(rotation);
var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2;

View File

@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
HitObjects =
{
new DrumRoll { Duration = 2000 }
new Swell { Duration = 2000 }
}
};
@ -172,5 +172,85 @@ namespace osu.Game.Rulesets.Taiko.Tests
Assert.That(healthProcessor.HasFailed, Is.False);
});
}
[Test]
public void TestMissHitAndHitSwell()
{
var beatmap = new TaikoBeatmap
{
HitObjects =
{
new Hit(),
new Swell { Duration = 2000 }
}
};
foreach (var ho in beatmap.HitObjects)
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
var healthProcessor = new TaikoHealthProcessor();
healthProcessor.ApplyBeatmap(beatmap);
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Miss });
foreach (var nested in beatmap.HitObjects[1].NestedHitObjects)
{
var nestedJudgement = nested.CreateJudgement();
healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult });
}
var judgement = beatmap.HitObjects[1].CreateJudgement();
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], judgement) { Type = judgement.MaxResult });
Assert.Multiple(() =>
{
Assert.That(healthProcessor.Health.Value, Is.EqualTo(0));
Assert.That(healthProcessor.HasFailed, Is.True);
});
}
private static readonly object[][] test_cases =
[
// hitobject, fail expected after miss
[new Hit(), true],
[new Hit.StrongNestedHit(new Hit()), false],
[new DrumRollTick(new DrumRoll()), false],
[new DrumRollTick.StrongNestedHit(new DrumRollTick(new DrumRoll())), false],
[new DrumRoll(), false],
[new SwellTick(), false],
[new Swell(), false]
];
[TestCaseSource(nameof(test_cases))]
public void TestFailAfterMinResult(TaikoHitObject hitObject, bool failExpected)
{
var healthProcessor = new TaikoHealthProcessor();
healthProcessor.ApplyBeatmap(new TaikoBeatmap
{
HitObjects = { hitObject }
});
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MinResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.EqualTo(failExpected));
}
[TestCaseSource(nameof(test_cases))]
public void TestNoFailAfterMaxResult(TaikoHitObject hitObject, bool _)
{
var healthProcessor = new TaikoHealthProcessor();
healthProcessor.ApplyBeatmap(new TaikoBeatmap
{
HitObjects = { hitObject }
});
var result = new JudgementResult(hitObject, hitObject.CreateJudgement());
result.Type = result.Judgement.MaxResult;
healthProcessor.ApplyResult(result);
Assert.That(healthProcessor.HasFailed, Is.False);
}
}
}

View File

@ -432,7 +432,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
CultureInfo.CurrentCulture = originalCulture;
}
private class TestLegacyScoreDecoder : LegacyScoreDecoder
public class TestLegacyScoreDecoder : LegacyScoreDecoder
{
private readonly int beatmapVersion;

View File

@ -0,0 +1,55 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using NUnit.Framework;
using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Beatmaps.Formats
{
public class LegacyScoreEncoderTest
{
[TestCase(1, 3)]
[TestCase(1, 0)]
[TestCase(0, 3)]
public void CatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount)
{
var ruleset = new CatchRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
var beatmap = new TestBeatmap(ruleset);
scoreInfo.Statistics = new Dictionary<HitResult, int>
{
[HitResult.Great] = 50,
[HitResult.LargeTickHit] = 5,
[HitResult.Miss] = missCount,
[HitResult.LargeTickMiss] = largeTickMissCount
};
var score = new Score { ScoreInfo = scoreInfo };
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
Assert.That(decodedAfterEncode.ScoreInfo.GetCountMiss(), Is.EqualTo(missCount + largeTickMissCount));
}
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
{
var encodeStream = new MemoryStream();
var encoder = new LegacyScoreEncoder(score, beatmap);
encoder.Encode(encodeStream);
var decodeStream = new MemoryStream(encodeStream.GetBuffer());
var decoder = new LegacyScoreDecoderTest.TestLegacyScoreDecoder(beatmapVersion);
var decodedAfterEncode = decoder.Parse(decodeStream);
return decodedAfterEncode;
}
}
}

View File

@ -1,26 +1,109 @@
// 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 Moq;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Online.Notifications.WebSocket.Events;
using osu.Game.Overlays;
using osu.Game.Users;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
[TestFixture]
public partial class TestSceneMedalOverlay : OsuTestScene
public partial class TestSceneMedalOverlay : OsuManualInputManagerTestScene
{
public TestSceneMedalOverlay()
private readonly Bindable<OverlayActivation> overlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
private MedalOverlay overlay = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep(@"display", () =>
var overlayManagerMock = new Mock<IOverlayManager>();
overlayManagerMock.Setup(mock => mock.OverlayActivationMode).Returns(overlayActivationMode);
AddStep("create overlay", () => Child = new DependencyProvidingContainer
{
LoadComponentAsync(new MedalOverlay(new Medal
{
Name = @"Animations",
InternalName = @"all-intro-doubletime",
Description = @"More complex than you think.",
}), Add);
Child = overlay = new MedalOverlay(),
RelativeSizeAxes = Axes.Both,
CachedDependencies =
[
(typeof(IOverlayManager), overlayManagerMock.Object)
]
});
}
[Test]
public void TestBasicAward()
{
awardMedal(new UserAchievementUnlock
{
Title = "Time And A Half",
Description = "Having a right ol' time. One and a half of them, almost.",
Slug = @"all-intro-doubletime"
});
AddUntilStep("overlay shown", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
AddUntilStep("wait for load", () => this.ChildrenOfType<MedalAnimation>().Any());
AddRepeatStep("dismiss", () => InputManager.Key(Key.Escape), 2);
AddUntilStep("overlay hidden", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
}
[Test]
public void TestMultipleMedalsInQuickSuccession()
{
awardMedal(new UserAchievementUnlock
{
Title = "Time And A Half",
Description = "Having a right ol' time. One and a half of them, almost.",
Slug = @"all-intro-doubletime"
});
awardMedal(new UserAchievementUnlock
{
Title = "S-Ranker",
Description = "Accuracy is really underrated.",
Slug = @"all-secret-rank-s"
});
awardMedal(new UserAchievementUnlock
{
Title = "500 Combo",
Description = "500 big ones! You're moving up in the world!",
Slug = @"osu-combo-500"
});
}
[Test]
public void TestDelayMedalDisplayUntilActivationModeAllowsIt()
{
AddStep("disable overlay activation", () => overlayActivationMode.Value = OverlayActivation.Disabled);
awardMedal(new UserAchievementUnlock
{
Title = "Time And A Half",
Description = "Having a right ol' time. One and a half of them, almost.",
Slug = @"all-intro-doubletime"
});
AddUntilStep("overlay hidden", () => overlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("re-enable overlay activation", () => overlayActivationMode.Value = OverlayActivation.All);
AddUntilStep("overlay shown", () => overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
}
private void awardMedal(UserAchievementUnlock unlock) => AddStep("award medal", () => dummyAPI.NotificationsClient.Receive(new SocketMessage
{
Event = @"new",
Data = JObject.FromObject(new NewPrivateNotificationEvent
{
Name = @"user_achievement_unlock",
Details = JObject.FromObject(unlock)
})
}));
}
}

View File

@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private TextureUpload upscale(TextureUpload textureUpload)
{
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
var image = Image.LoadPixelData(textureUpload.Data, textureUpload.Width, textureUpload.Height);
// The original texture upload will no longer be returned or used.
textureUpload.Dispose();

View File

@ -22,6 +22,7 @@ using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko;
@ -286,6 +287,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
[Test]
[FlakyTest] // See above
public void TestModSelectOverlay()
{
AddStep("add playlist item", () =>
{
SelectedRoom.Value.Playlist.Add(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
RequiredMods = new[]
{
new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 2.0 } }),
new APIMod(new OsuModStrictTracking()),
},
AllowedMods = new[]
{
new APIMod(new OsuModFlashlight()),
}
});
});
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
AddUntilStep("wait for join", () => RoomJoined);
ClickButtonWhenEnabled<RoomSubScreen.UserModSelectButton>();
AddAssert("mod select shows unranked", () => screen.UserModsSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().Ranked.Value == false);
AddAssert("score multiplier = 1.20", () => screen.UserModsSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
AddStep("select flashlight", () => screen.UserModsSelectOverlay.ChildrenOfType<ModPanel>().Single(m => m.Mod is ModFlashlight).TriggerClick());
AddAssert("score multiplier = 1.35", () => screen.UserModsSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.35).Within(0.01));
AddStep("change flashlight setting", () => ((OsuModFlashlight)screen.UserModsSelectOverlay.SelectedMods.Value.Single()).FollowDelay.Value = 1200);
AddAssert("score multiplier = 1.20", () => screen.UserModsSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
}
private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen
{
[Resolved(canBeNull: true)]

View File

@ -6,6 +6,7 @@
using System;
using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
@ -24,6 +25,8 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Leaderboards;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Online.Notifications.WebSocket.Events;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.Mods;
@ -340,6 +343,28 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen);
}
[Test]
public void TestShowMedalAtResults()
{
playToResults();
AddStep("award medal", () => ((DummyAPIAccess)API).NotificationsClient.Receive(new SocketMessage
{
Event = @"new",
Data = JObject.FromObject(new NewPrivateNotificationEvent
{
Name = @"user_achievement_unlock",
Details = JObject.FromObject(new UserAchievementUnlock
{
Title = "Time And A Half",
Description = "Having a right ol' time. One and a half of them, almost.",
Slug = @"all-intro-doubletime"
})
})
}));
AddUntilStep("medal overlay shown", () => Game.ChildrenOfType<MedalOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
}
[Test]
public void TestRetryFromResults()
{

View File

@ -0,0 +1,39 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.Leaderboards;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneDrawableRank : OsuTestScene
{
[Test]
public void TestAllRanks()
{
AddStep("create content", () => Child = new FillFlowContainer<DrawableRank>
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Direction = FillDirection.Vertical,
Padding = new MarginPadding(20),
Spacing = new Vector2(10),
ChildrenEnumerable = Enum.GetValues<ScoreRank>().OrderBy(v => v).Select(rank => new DrawableRank(rank)
{
RelativeSizeAxes = Axes.None,
Size = new Vector2(50, 25),
Anchor = Anchor.Centre,
Origin = Anchor.Centre
})
});
}
}
}

View File

@ -88,8 +88,20 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("play time not displayed", () => !this.ChildrenOfType<ExpandedPanelMiddleContent.PlayedOnText>().Any());
}
private void showPanel(ScoreInfo score) =>
Child = new ExpandedPanelMiddleContentContainer(score);
[Test]
public void TestFailedSDisplay([Values] bool withFlair)
{
AddStep("show failed S score", () =>
{
var score = TestResources.CreateTestScoreInfo(createTestBeatmap(new RealmUser()));
score.Rank = ScoreRank.A;
score.Accuracy = 0.975;
showPanel(score, withFlair);
});
}
private void showPanel(ScoreInfo score, bool withFlair = false) =>
Child = new ExpandedPanelMiddleContentContainer(score, withFlair);
private BeatmapInfo createTestBeatmap([NotNull] RealmUser author)
{
@ -107,7 +119,7 @@ namespace osu.Game.Tests.Visual.Ranking
private partial class ExpandedPanelMiddleContentContainer : Container
{
public ExpandedPanelMiddleContentContainer(ScoreInfo score)
public ExpandedPanelMiddleContentContainer(ScoreInfo score, bool withFlair)
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
@ -119,7 +131,7 @@ namespace osu.Game.Tests.Visual.Ranking
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#444"),
},
new ExpandedPanelMiddleContent(score)
new ExpandedPanelMiddleContent(score, withFlair)
};
}
}

View File

@ -57,6 +57,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Mods = { BindTarget = SelectedMods },
});
AddStep("set beatmap", () =>

View File

@ -859,6 +859,30 @@ namespace osu.Game.Tests.Visual.UserInterface
() => modSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON));
}
[Test]
public void TestModSettingsOrder()
{
createScreen();
AddStep("select DT + HD + DF", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModDeflate() });
AddAssert("mod settings order: DT, HD, DF", () =>
{
var columns = this.ChildrenOfType<ModSettingsArea>().Single().ChildrenOfType<ModSettingsArea.ModSettingsColumn>();
return columns.ElementAt(0).Mod is OsuModDoubleTime &&
columns.ElementAt(1).Mod is OsuModHidden &&
columns.ElementAt(2).Mod is OsuModDeflate;
});
AddStep("replace DT with NC", () => SelectedMods.Value = SelectedMods.Value.Where(m => m is not ModDoubleTime).Append(new OsuModNightcore()).ToList());
AddAssert("mod settings order: NC, HD, DF", () =>
{
var columns = this.ChildrenOfType<ModSettingsArea>().Single().ChildrenOfType<ModSettingsArea.ModSettingsColumn>();
return columns.ElementAt(0).Mod is OsuModNightcore &&
columns.ElementAt(1).Mod is OsuModHidden &&
columns.ElementAt(2).Mod is OsuModDeflate;
});
}
private void waitForColumnLoad() => AddUntilStep("all column content loaded", () =>
modSelectOverlay.ChildrenOfType<ModColumn>().Any()
&& modSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded)

View File

@ -64,7 +64,7 @@ namespace osu.Game.Beatmaps
// The original texture upload will no longer be returned or used.
textureUpload.Dispose();
Size size = image.Size();
Size size = image.Size;
// Assume that panel backgrounds are always displayed using `FillMode.Fill`.
// Also assume that all backgrounds are wider than they are tall, so the

View File

@ -86,11 +86,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Dimmed.BindValueChanged(_ => updateState());
playButton.Playing.BindValueChanged(_ => updateState(), true);
((IBindable<double>)progress.Current).BindTo(playButton.Progress);
FinishTransforms(true);
}
protected override void Update()
{
base.Update();
progress.Progress = playButton.Progress.Value;
}
private void updateState()
{
bool shouldDim = Dimmed.Value || playButton.Playing.Value;

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps.Legacy;
using osu.Game.IO;
using osu.Game.Storyboards;
@ -230,7 +229,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.Rotation.Add(easing, startTime, endTime, MathUtils.RadiansToDegrees(startValue), MathUtils.RadiansToDegrees(endValue));
timelineGroup?.Rotation.Add(easing, startTime, endTime, float.RadiansToDegrees(startValue), float.RadiansToDegrees(endValue));
break;
}

View File

@ -23,8 +23,7 @@ namespace osu.Game.Beatmaps.Timing
public TimeSignature(int numerator)
{
if (numerator < 1)
throw new ArgumentOutOfRangeException(nameof(numerator), numerator, "The numerator of a time signature must be positive.");
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(numerator);
Numerator = numerator;
}

View File

@ -489,8 +489,7 @@ namespace osu.Game.Database
/// <param name="action">The work to run.</param>
public Task WriteAsync(Action<Realm> action)
{
if (isDisposed)
throw new ObjectDisposedException(nameof(RealmAccess));
ObjectDisposedException.ThrowIf(isDisposed, this);
// Required to ensure the write is tracked and accounted for before disposal.
// Can potentially be avoided if we have a need to do so in the future.
@ -675,8 +674,7 @@ namespace osu.Game.Database
private Realm getRealmInstance()
{
if (isDisposed)
throw new ObjectDisposedException(nameof(RealmAccess));
ObjectDisposedException.ThrowIf(isDisposed, this);
bool tookSemaphoreLock = false;
@ -1189,8 +1187,7 @@ namespace osu.Game.Database
if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread.");
if (isDisposed)
throw new ObjectDisposedException(nameof(RealmAccess));
ObjectDisposedException.ThrowIf(isDisposed, this);
SynchronizationContext? syncContext = null;

View File

@ -415,7 +415,7 @@ namespace osu.Game.Database
// Calculate how many times the longest combo the user has achieved in the play can repeat
// without exceeding the combo portion in score V1 as achieved by the player.
// This is a pessimistic estimate; it intentionally does not operate on object count and uses only score instead.
// This intentionally does not operate on object count and uses only score instead.
double maximumOccurrencesOfLongestCombo = Math.Floor(comboPortionInScoreV1 / comboPortionFromLongestComboInScoreV1);
double comboPortionFromRepeatedLongestCombosInScoreV1 = maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInScoreV1;
@ -426,13 +426,12 @@ namespace osu.Game.Database
// ...and then based on that raw combo length, we calculate how much this last combo is worth in standardised score.
double remainingComboPortionInStandardisedScore = Math.Pow(remainingCombo, 1 + ScoreProcessor.COMBO_EXPONENT);
double lowerEstimateOfComboPortionInStandardisedScore
double scoreBasedEstimateOfComboPortionInStandardisedScore
= maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInStandardisedScore
+ remainingComboPortionInStandardisedScore;
// Compute approximate upper estimate new score for that play.
// This time, divide the remaining combo among remaining objects equally to achieve longest possible combo lengths.
// There is no rigorous proof that doing this will yield a correct upper bound, but it seems to work out in practice.
remainingComboPortionInScoreV1 = comboPortionInScoreV1 - comboPortionFromLongestComboInScoreV1;
double remainingCountOfObjectsGivingCombo = maximumLegacyCombo - score.MaxCombo - score.Statistics.GetValueOrDefault(HitResult.Miss);
// Because we assumed all combos were equal, `remainingComboPortionInScoreV1`
@ -449,7 +448,17 @@ namespace osu.Game.Database
// we can skip adding the 1 and just multiply by x ^ 0.5.
remainingComboPortionInStandardisedScore = remainingCountOfObjectsGivingCombo * Math.Pow(lengthOfRemainingCombos, ScoreProcessor.COMBO_EXPONENT);
double upperEstimateOfComboPortionInStandardisedScore = comboPortionFromLongestComboInStandardisedScore + remainingComboPortionInStandardisedScore;
double objectCountBasedEstimateOfComboPortionInStandardisedScore = comboPortionFromLongestComboInStandardisedScore + remainingComboPortionInStandardisedScore;
// Enforce some invariants on both of the estimates.
// In rare cases they can produce invalid results.
scoreBasedEstimateOfComboPortionInStandardisedScore =
Math.Clamp(scoreBasedEstimateOfComboPortionInStandardisedScore, 0, maximumAchievableComboPortionInStandardisedScore);
objectCountBasedEstimateOfComboPortionInStandardisedScore =
Math.Clamp(objectCountBasedEstimateOfComboPortionInStandardisedScore, 0, maximumAchievableComboPortionInStandardisedScore);
double lowerEstimateOfComboPortionInStandardisedScore = Math.Min(scoreBasedEstimateOfComboPortionInStandardisedScore, objectCountBasedEstimateOfComboPortionInStandardisedScore);
double upperEstimateOfComboPortionInStandardisedScore = Math.Max(scoreBasedEstimateOfComboPortionInStandardisedScore, objectCountBasedEstimateOfComboPortionInStandardisedScore);
// Approximate by combining lower and upper estimates.
// As the lower-estimate is very pessimistic, we use a 30/70 ratio

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@ -20,10 +18,10 @@ namespace osu.Game.Graphics.Containers
[Cached(typeof(IPreviewTrackOwner))]
public abstract partial class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler<GlobalAction>
{
private Sample samplePopIn;
private Sample samplePopOut;
protected virtual string PopInSampleName => "UI/overlay-pop-in";
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
protected readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
protected virtual string? PopInSampleName => @"UI/overlay-pop-in";
protected virtual string? PopOutSampleName => @"UI/overlay-pop-out";
protected virtual double PopInOutSampleBalance => 0;
protected override bool BlockNonPositionalInput => true;
@ -34,19 +32,23 @@ namespace osu.Game.Graphics.Containers
/// </summary>
protected virtual bool DimMainContent => true;
[Resolved(CanBeNull = true)]
private IOverlayManager overlayManager { get; set; }
[Resolved]
private IOverlayManager? overlayManager { get; set; }
[Resolved]
private PreviewTrackManager previewTrackManager { get; set; }
private PreviewTrackManager previewTrackManager { get; set; } = null!;
protected readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
private Sample? samplePopIn;
private Sample? samplePopOut;
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio)
[BackgroundDependencyLoader]
private void load(AudioManager? audio)
{
samplePopIn = audio.Samples.Get(PopInSampleName);
samplePopOut = audio.Samples.Get(PopOutSampleName);
if (!string.IsNullOrEmpty(PopInSampleName))
samplePopIn = audio?.Samples.Get(PopInSampleName);
if (!string.IsNullOrEmpty(PopOutSampleName))
samplePopOut = audio?.Samples.Get(PopOutSampleName);
}
protected override void LoadComplete()

View File

@ -157,7 +157,7 @@ namespace osu.Game.Graphics.Cursor
if (dragRotationState == DragRotationState.Rotating && distance > 0)
{
Vector2 offset = e.MousePosition - positionMouseDown;
float degrees = MathUtils.RadiansToDegrees(MathF.Atan2(-offset.X, offset.Y)) + 24.3f;
float degrees = float.RadiansToDegrees(MathF.Atan2(-offset.X, offset.Y)) + 24.3f;
// Always rotate in the direction of least distance
float diff = (degrees - activeCursor.Rotation) % 360;

View File

@ -63,8 +63,12 @@ namespace osu.Game.Graphics
case ScoreRank.C:
return Color4Extensions.FromHex(@"ff8e5d");
default:
case ScoreRank.D:
return Color4Extensions.FromHex(@"ff5a5a");
case ScoreRank.F:
default:
return Color4Extensions.FromHex(@"3f3f3f");
}
}

View File

@ -1,14 +1,12 @@
// 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.Extensions;
namespace osu.Game.Graphics.UserInterface
{
public partial class OsuNumberBox : OsuTextBox
{
protected override bool AllowIme => false;
protected override bool CanAddCharacter(char character) => character.IsAsciiDigit();
protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character);
}
}

View File

@ -116,18 +116,18 @@ namespace osu.Game.Graphics.UserInterface
}
}
private const float transition_length = 500;
protected const float TRANSITION_LENGTH = 500;
protected void FadeHovered()
protected virtual void FadeHovered()
{
Bar.FadeIn(transition_length, Easing.OutQuint);
Text.FadeColour(Color4.White, transition_length, Easing.OutQuint);
Bar.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
Text.FadeColour(Color4.White, TRANSITION_LENGTH, Easing.OutQuint);
}
protected void FadeUnhovered()
protected virtual void FadeUnhovered()
{
Bar.FadeTo(IsHovered ? 1 : 0, transition_length, Easing.OutQuint);
Text.FadeColour(IsHovered ? Color4.White : AccentColour, transition_length, Easing.OutQuint);
Bar.FadeTo(IsHovered ? 1 : 0, TRANSITION_LENGTH, Easing.OutQuint);
Text.FadeColour(IsHovered ? Color4.White : AccentColour, TRANSITION_LENGTH, Easing.OutQuint);
}
protected override bool OnHover(HoverEvent e)

View File

@ -8,7 +8,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Toolkit.HighPerformance;
using osu.Framework.Extensions;
using osu.Framework.IO.Stores;
using SharpCompress.Archives.Zip;
using SixLabors.ImageSharp.Memory;
@ -36,7 +35,7 @@ namespace osu.Game.IO.Archives
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);
using (Stream s = entry.OpenEntryStream())
s.ReadToFill(owner.Memory.Span);
s.ReadExactly(owner.Memory.Span);
return new MemoryOwnerMemoryStream(owner);
}

View File

@ -80,8 +80,7 @@ namespace osu.Game.IO
public override Storage GetStorageForDirectory(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Must be non-null and not empty string", nameof(path));
ArgumentException.ThrowIfNullOrEmpty(path);
if (!path.EndsWith(Path.DirectorySeparatorChar))
path += Path.DirectorySeparatorChar;

View File

@ -245,8 +245,8 @@ namespace osu.Game.Online.API.Requests.Responses
RulesetID = score.RulesetID,
Passed = score.Passed,
Mods = score.APIMods,
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(),
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(),
};
}
}

View File

@ -13,6 +13,8 @@ using osu.Framework.Logging;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Online.Notifications.WebSocket.Events;
using osu.Game.Online.Notifications.WebSocket.Requests;
namespace osu.Game.Online.Chat
{

View File

@ -95,8 +95,12 @@ namespace osu.Game.Online.Leaderboards
case ScoreRank.C:
return Color4Extensions.FromHex(@"473625");
default:
case ScoreRank.D:
return Color4Extensions.FromHex(@"512525");
case ScoreRank.F:
default:
return Color4Extensions.FromHex(@"CC3333");
}
}
}

View File

@ -8,7 +8,7 @@ using Newtonsoft.Json;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
namespace osu.Game.Online.Notifications.WebSocket
namespace osu.Game.Online.Notifications.WebSocket.Events
{
/// <summary>
/// A websocket message sent from the server when new messages arrive.

View File

@ -0,0 +1,39 @@
// 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 Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace osu.Game.Online.Notifications.WebSocket.Events
{
/// <summary>
/// Reference: https://github.com/ppy/osu-web/blob/master/app/Events/NewPrivateNotificationEvent.php
/// </summary>
public class NewPrivateNotificationEvent
{
[JsonProperty("id")]
public ulong ID { get; set; }
[JsonProperty("name")]
public string Name { get; set; } = string.Empty;
[JsonProperty("created_at")]
public DateTimeOffset CreatedAt { get; set; }
[JsonProperty("object_type")]
public string ObjectType { get; set; } = string.Empty;
[JsonProperty("object_id")]
public ulong ObjectId { get; set; }
[JsonProperty("source_user_id")]
public uint SourceUserID { get; set; }
[JsonProperty("is_read")]
public bool IsRead { get; set; }
[JsonProperty("details")]
public JObject? Details { get; set; }
}
}

View File

@ -0,0 +1,34 @@
// 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 Newtonsoft.Json;
namespace osu.Game.Online.Notifications.WebSocket.Events
{
/// <summary>
/// Reference: https://github.com/ppy/osu-web/blob/master/app/Jobs/Notifications/UserAchievementUnlock.php
/// </summary>
public class UserAchievementUnlock
{
[JsonProperty("achievement_id")]
public uint AchievementId { get; set; }
[JsonProperty("achievement_mode")]
public ushort? AchievementMode { get; set; }
[JsonProperty("cover_url")]
public string CoverUrl { get; set; } = string.Empty;
[JsonProperty("slug")]
public string Slug { get; set; } = string.Empty;
[JsonProperty("title")]
public string Title { get; set; } = string.Empty;
[JsonProperty("description")]
public string Description { get; set; } = string.Empty;
[JsonProperty("user_id")]
public uint UserId { get; set; }
}
}

View File

@ -3,7 +3,7 @@
using Newtonsoft.Json;
namespace osu.Game.Online.Notifications.WebSocket
namespace osu.Game.Online.Notifications.WebSocket.Requests
{
/// <summary>
/// A websocket message notifying the server that the client no longer wants to receive chat messages.

View File

@ -3,7 +3,7 @@
using Newtonsoft.Json;
namespace osu.Game.Online.Notifications.WebSocket
namespace osu.Game.Online.Notifications.WebSocket.Requests
{
/// <summary>
/// A websocket message notifying the server that the client wants to receive chat messages.

View File

@ -1083,6 +1083,7 @@ namespace osu.Game
loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile<IDialogOverlay>(new DialogOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile(new MedalOverlay(), topMostOverlayContent.Add);
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);

View File

@ -0,0 +1,312 @@
// 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 osuTK;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Sprites;
using osu.Game.Users;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Overlays.MedalSplash;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio;
using osu.Framework.Graphics.Textures;
using osu.Framework.Graphics.Shapes;
using System;
using System.Diagnostics;
using osu.Framework.Graphics.Effects;
using osu.Framework.Utils;
namespace osu.Game.Overlays
{
public partial class MedalAnimation : VisibilityContainer
{
public const float DISC_SIZE = 400;
private const float border_width = 5;
private readonly Medal medal;
private readonly Box background;
private readonly Container backgroundStrip, particleContainer;
private readonly BackgroundStrip leftStrip, rightStrip;
private readonly CircularContainer disc;
private readonly Sprite innerSpin, outerSpin;
private DrawableMedal? drawableMedal;
private Sample? getSample;
private readonly Container content;
public MedalAnimation(Medal medal)
{
this.medal = medal;
RelativeSizeAxes = Axes.Both;
Child = content = new Container
{
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(60),
},
outerSpin = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(DISC_SIZE + 500),
Alpha = 0f,
},
backgroundStrip = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = border_width,
Alpha = 0f,
Children = new[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
Width = 0.5f,
Padding = new MarginPadding { Right = DISC_SIZE / 2 },
Children = new[]
{
leftStrip = new BackgroundStrip(0f, 1f)
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
},
},
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
Width = 0.5f,
Padding = new MarginPadding { Left = DISC_SIZE / 2 },
Children = new[]
{
rightStrip = new BackgroundStrip(1f, 0f),
},
},
},
},
particleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Alpha = 0f,
},
disc = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0f,
Masking = true,
AlwaysPresent = true,
BorderColour = Color4.White,
BorderThickness = border_width,
Size = new Vector2(DISC_SIZE),
Scale = new Vector2(0.8f),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"05262f"),
},
new Triangles
{
RelativeSizeAxes = Axes.Both,
TriangleScale = 2,
ColourDark = Color4Extensions.FromHex(@"04222b"),
ColourLight = Color4Extensions.FromHex(@"052933"),
},
innerSpin = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(1.05f),
Alpha = 0.25f,
},
},
},
}
};
Show();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, TextureStore textures, AudioManager audio)
{
getSample = audio.Samples.Get(@"MedalSplash/medal-get");
innerSpin.Texture = outerSpin.Texture = textures.Get(@"MedalSplash/disc-spin");
disc.EdgeEffect = leftStrip.EdgeEffect = rightStrip.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = colours.Blue.Opacity(0.5f),
Radius = 50,
};
}
protected override void LoadComplete()
{
base.LoadComplete();
LoadComponentAsync(drawableMedal = new DrawableMedal(medal)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
}, loaded =>
{
disc.Add(loaded);
startAnimation();
});
}
protected override void Update()
{
base.Update();
particleContainer.Add(new MedalParticle(RNG.Next(0, 359)));
}
private const double initial_duration = 400;
private const double step_duration = 900;
private void startAnimation()
{
content.Show();
background.FlashColour(Color4.White.Opacity(0.25f), 400);
getSample?.Play();
innerSpin.Spin(20000, RotationDirection.Clockwise);
outerSpin.Spin(40000, RotationDirection.Clockwise);
using (BeginDelayedSequence(200))
{
disc.FadeIn(initial_duration)
.ScaleTo(1f, initial_duration * 2, Easing.OutElastic);
particleContainer.FadeIn(initial_duration);
outerSpin.FadeTo(0.1f, initial_duration * 2);
using (BeginDelayedSequence(initial_duration + 200))
{
backgroundStrip.FadeIn(step_duration);
leftStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint);
rightStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint);
Debug.Assert(drawableMedal != null);
this.Animate().Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.Icon;
}).Delay(step_duration).Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.MedalUnlocked;
}).Delay(step_duration).Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.Full;
});
}
}
}
protected override void PopIn()
{
this.FadeIn(200);
}
protected override void PopOut()
{
this.FadeOut(200);
}
public void Dismiss()
{
if (drawableMedal != null && drawableMedal.State != DisplayState.Full)
{
// if we haven't yet, play out the animation fully
drawableMedal.State = DisplayState.Full;
FinishTransforms(true);
return;
}
Hide();
Expire();
}
private partial class BackgroundStrip : Container
{
public BackgroundStrip(float start, float end)
{
RelativeSizeAxes = Axes.Both;
Width = 0f;
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(start), Color4.White.Opacity(end));
Masking = true;
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
}
};
}
}
private partial class MedalParticle : CircularContainer
{
private readonly float direction;
private Vector2 positionForOffset(float offset) => new Vector2((float)(offset * Math.Sin(direction)), (float)(offset * Math.Cos(direction)));
public MedalParticle(float direction)
{
this.direction = direction;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Position = positionForOffset(DISC_SIZE / 2);
Masking = true;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = colours.Blue.Opacity(0.5f),
Radius = 5,
};
this.MoveTo(positionForOffset(DISC_SIZE / 2 + 200), 500);
this.FadeOut(500);
Expire();
}
}
}
}

View File

@ -1,324 +1,130 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osuTK;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Sprites;
using osu.Game.Users;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Overlays.MedalSplash;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio;
using osu.Framework.Graphics.Textures;
using osuTK.Input;
using osu.Framework.Graphics.Shapes;
using System;
using osu.Framework.Graphics.Effects;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Online.API;
using osu.Game.Online.Notifications.WebSocket;
using osu.Game.Online.Notifications.WebSocket.Events;
using osu.Game.Users;
namespace osu.Game.Overlays
{
public partial class MedalOverlay : FocusedOverlayContainer
public partial class MedalOverlay : OsuFocusedOverlayContainer
{
public const float DISC_SIZE = 400;
protected override string? PopInSampleName => null;
protected override string? PopOutSampleName => null;
private const float border_width = 5;
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
private readonly Medal medal;
private readonly Box background;
private readonly Container backgroundStrip, particleContainer;
private readonly BackgroundStrip leftStrip, rightStrip;
private readonly CircularContainer disc;
private readonly Sprite innerSpin, outerSpin;
private DrawableMedal drawableMedal;
protected override void PopIn() => this.FadeIn();
private Sample getSample;
protected override void PopOut() => this.FadeOut();
private readonly Container content;
private readonly Queue<MedalAnimation> queuedMedals = new Queue<MedalAnimation>();
public MedalOverlay(Medal medal)
{
this.medal = medal;
RelativeSizeAxes = Axes.Both;
[Resolved]
private IAPIProvider api { get; set; } = null!;
Child = content = new Container
{
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(60),
},
outerSpin = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(DISC_SIZE + 500),
Alpha = 0f,
},
backgroundStrip = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = border_width,
Alpha = 0f,
Children = new[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
Width = 0.5f,
Padding = new MarginPadding { Right = DISC_SIZE / 2 },
Children = new[]
{
leftStrip = new BackgroundStrip(0f, 1f)
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
},
},
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
Width = 0.5f,
Padding = new MarginPadding { Left = DISC_SIZE / 2 },
Children = new[]
{
rightStrip = new BackgroundStrip(1f, 0f),
},
},
},
},
particleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Alpha = 0f,
},
disc = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0f,
Masking = true,
AlwaysPresent = true,
BorderColour = Color4.White,
BorderThickness = border_width,
Size = new Vector2(DISC_SIZE),
Scale = new Vector2(0.8f),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"05262f"),
},
new Triangles
{
RelativeSizeAxes = Axes.Both,
TriangleScale = 2,
ColourDark = Color4Extensions.FromHex(@"04222b"),
ColourLight = Color4Extensions.FromHex(@"052933"),
},
innerSpin = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(1.05f),
Alpha = 0.25f,
},
},
},
}
};
Show();
}
private Container<Drawable> medalContainer = null!;
private MedalAnimation? lastAnimation;
[BackgroundDependencyLoader]
private void load(OsuColour colours, TextureStore textures, AudioManager audio)
private void load()
{
getSample = audio.Samples.Get(@"MedalSplash/medal-get");
innerSpin.Texture = outerSpin.Texture = textures.Get(@"MedalSplash/disc-spin");
RelativeSizeAxes = Axes.Both;
disc.EdgeEffect = leftStrip.EdgeEffect = rightStrip.EdgeEffect = new EdgeEffectParameters
api.NotificationsClient.MessageReceived += handleMedalMessages;
Add(medalContainer = new Container
{
Type = EdgeEffectType.Glow,
Colour = colours.Blue.Opacity(0.5f),
Radius = 50,
};
RelativeSizeAxes = Axes.Both
});
}
protected override void LoadComplete()
{
base.LoadComplete();
LoadComponentAsync(drawableMedal = new DrawableMedal(medal)
OverlayActivationMode.BindValueChanged(val =>
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
}, loaded =>
if (val.NewValue == OverlayActivation.All && (queuedMedals.Any() || medalContainer.Any() || lastAnimation?.IsLoaded == false))
Show();
}, true);
}
private void handleMedalMessages(SocketMessage obj)
{
disc.Add(loaded);
startAnimation();
});
if (obj.Event != @"new")
return;
var data = obj.Data?.ToObject<NewPrivateNotificationEvent>();
if (data == null || data.Name != @"user_achievement_unlock")
return;
var details = data.Details?.ToObject<UserAchievementUnlock>();
if (details == null)
return;
var medal = new Medal
{
Name = details.Title,
InternalName = details.Slug,
Description = details.Description,
};
var medalAnimation = new MedalAnimation(medal);
queuedMedals.Enqueue(medalAnimation);
if (OverlayActivationMode.Value == OverlayActivation.All)
Scheduler.AddOnce(Show);
}
protected override void Update()
{
base.Update();
particleContainer.Add(new MedalParticle(RNG.Next(0, 359)));
if (medalContainer.Any() || lastAnimation?.IsLoaded == false)
return;
if (!queuedMedals.TryDequeue(out lastAnimation))
{
Hide();
return;
}
LoadComponentAsync(lastAnimation, medalContainer.Add);
}
protected override bool OnClick(ClickEvent e)
{
dismiss();
lastAnimation?.Dismiss();
return true;
}
protected override void OnFocusLost(FocusLostEvent e)
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.CurrentState.Keyboard.Keys.IsPressed(Key.Escape)) dismiss();
if (e.Action == GlobalAction.Back)
{
lastAnimation?.Dismiss();
return true;
}
private const double initial_duration = 400;
private const double step_duration = 900;
private void startAnimation()
{
content.Show();
background.FlashColour(Color4.White.Opacity(0.25f), 400);
getSample.Play();
innerSpin.Spin(20000, RotationDirection.Clockwise);
outerSpin.Spin(40000, RotationDirection.Clockwise);
using (BeginDelayedSequence(200))
{
disc.FadeIn(initial_duration)
.ScaleTo(1f, initial_duration * 2, Easing.OutElastic);
particleContainer.FadeIn(initial_duration);
outerSpin.FadeTo(0.1f, initial_duration * 2);
using (BeginDelayedSequence(initial_duration + 200))
{
backgroundStrip.FadeIn(step_duration);
leftStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint);
rightStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint);
this.Animate().Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.Icon;
}).Delay(step_duration).Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.MedalUnlocked;
}).Delay(step_duration).Schedule(() =>
{
if (drawableMedal.State != DisplayState.Full)
drawableMedal.State = DisplayState.Full;
});
}
}
return base.OnPressed(e);
}
protected override void PopIn()
protected override void Dispose(bool isDisposing)
{
this.FadeIn(200);
}
base.Dispose(isDisposing);
protected override void PopOut()
{
this.FadeOut(200);
}
private void dismiss()
{
if (drawableMedal.State != DisplayState.Full)
{
// if we haven't yet, play out the animation fully
drawableMedal.State = DisplayState.Full;
FinishTransforms(true);
return;
}
Hide();
Expire();
}
private partial class BackgroundStrip : Container
{
public BackgroundStrip(float start, float end)
{
RelativeSizeAxes = Axes.Both;
Width = 0f;
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(start), Color4.White.Opacity(end));
Masking = true;
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
}
};
}
}
private partial class MedalParticle : CircularContainer
{
private readonly float direction;
private Vector2 positionForOffset(float offset) => new Vector2((float)(offset * Math.Sin(direction)), (float)(offset * Math.Cos(direction)));
public MedalParticle(float direction)
{
this.direction = direction;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Position = positionForOffset(DISC_SIZE / 2);
Masking = true;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = colours.Blue.Opacity(0.5f),
Radius = 5,
};
this.MoveTo(positionForOffset(DISC_SIZE / 2 + 200), 500);
this.FadeOut(500);
Expire();
}
if (api.IsNotNull())
api.NotificationsClient.MessageReceived -= handleMedalMessages;
}
}
}

View File

@ -38,7 +38,7 @@ namespace osu.Game.Overlays.MedalSplash
public DrawableMedal(Medal medal)
{
this.medal = medal;
Position = new Vector2(0f, MedalOverlay.DISC_SIZE / 2);
Position = new Vector2(0f, MedalAnimation.DISC_SIZE / 2);
FillFlowContainer infoFlow;
Children = new Drawable[]
@ -174,7 +174,7 @@ namespace osu.Game.Overlays.MedalSplash
.ScaleTo(1);
this.ScaleTo(scale_when_unlocked, duration, Easing.OutExpo);
this.MoveToY(MedalOverlay.DISC_SIZE / 2 - 30, duration, Easing.OutExpo);
this.MoveToY(MedalAnimation.DISC_SIZE / 2 - 30, duration, Easing.OutExpo);
unlocked.FadeInFromZero(duration);
break;
@ -184,7 +184,7 @@ namespace osu.Game.Overlays.MedalSplash
.ScaleTo(1);
this.ScaleTo(scale_when_full, duration, Easing.OutExpo);
this.MoveToY(MedalOverlay.DISC_SIZE / 2 - 60, duration, Easing.OutExpo);
this.MoveToY(MedalAnimation.DISC_SIZE / 2 - 60, duration, Easing.OutExpo);
unlocked.Show();
name.FadeInFromZero(duration + 100);
description.FadeInFromZero(duration * 2);

View File

@ -40,8 +40,7 @@ namespace osu.Game.Overlays.Mods
public Bindable<IBeatmapInfo?> BeatmapInfo { get; } = new Bindable<IBeatmapInfo?>();
[Resolved]
private Bindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
public Bindable<IReadOnlyList<Mod>> Mods { get; } = new Bindable<IReadOnlyList<Mod>>();
public BindableBool Collapsed { get; } = new BindableBool(true);
@ -53,7 +52,7 @@ namespace osu.Game.Overlays.Mods
[Resolved]
private OsuGameBase game { get; set; } = null!;
private IBindable<RulesetInfo> gameRuleset = null!;
protected IBindable<RulesetInfo> GameRuleset = null!;
private CancellationTokenSource? cancellationSource;
private IBindable<StarDifficulty?> starDifficulty = null!;
@ -101,15 +100,15 @@ namespace osu.Game.Overlays.Mods
{
base.LoadComplete();
mods.BindValueChanged(_ =>
Mods.BindValueChanged(_ =>
{
modSettingChangeTracker?.Dispose();
modSettingChangeTracker = new ModSettingChangeTracker(mods.Value);
modSettingChangeTracker = new ModSettingChangeTracker(Mods.Value);
modSettingChangeTracker.SettingChanged += _ => updateValues();
updateValues();
}, true);
BeatmapInfo.BindValueChanged(_ => updateValues(), true);
BeatmapInfo.BindValueChanged(_ => updateValues());
Collapsed.BindValueChanged(_ =>
{
@ -118,11 +117,12 @@ namespace osu.Game.Overlays.Mods
updateCollapsedState();
});
gameRuleset = game.Ruleset.GetBoundCopy();
gameRuleset.BindValueChanged(_ => updateValues());
GameRuleset = game.Ruleset.GetBoundCopy();
GameRuleset.BindValueChanged(_ => updateValues());
BeatmapInfo.BindValueChanged(_ => updateValues(), true);
BeatmapInfo.BindValueChanged(_ => updateValues());
updateValues();
updateCollapsedState();
}
@ -166,17 +166,17 @@ namespace osu.Game.Overlays.Mods
});
double rate = 1;
foreach (var mod in mods.Value.OfType<IApplicableToRate>())
foreach (var mod in Mods.Value.OfType<IApplicableToRate>())
rate = mod.ApplyToRate(0, rate);
bpmDisplay.Current.Value = FormatUtils.RoundBPM(BeatmapInfo.Value.BPM, rate);
BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty);
foreach (var mod in mods.Value.OfType<IApplicableToDifficulty>())
foreach (var mod in Mods.Value.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(originalDifficulty);
Ruleset ruleset = gameRuleset.Value.CreateInstance();
Ruleset ruleset = GameRuleset.Value.CreateInstance();
BeatmapDifficulty adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(originalDifficulty, rate);
TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty);
@ -195,7 +195,7 @@ namespace osu.Game.Overlays.Mods
RightContent.FadeTo(Collapsed.Value && !IsHovered ? 0 : 1, transition_duration, Easing.OutQuint);
}
private partial class BPMDisplay : RollingCounter<int>
public partial class BPMDisplay : RollingCounter<int>
{
protected override double RollingDuration => 250;

View File

@ -43,6 +43,14 @@ namespace osu.Game.Overlays.Mods
[Cached]
public Bindable<IReadOnlyList<Mod>> SelectedMods { get; private set; } = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
/// <summary>
/// Contains a list of mods which <see cref="ModSelectOverlay"/> should read from to display effects on the selected beatmap.
/// </summary>
/// <remarks>
/// This is different from <see cref="SelectedMods"/> in screens like online-play rooms, where there are required mods activated from the playlist.
/// </remarks>
public Bindable<IReadOnlyList<Mod>> ActiveMods { get; private set; } = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
/// <summary>
/// Contains a dictionary with the current <see cref="ModState"/> of all mods applicable for the current ruleset.
/// </summary>
@ -97,6 +105,8 @@ namespace osu.Game.Overlays.Mods
protected virtual IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection) => newSelection;
protected virtual IReadOnlyList<Mod> ComputeActiveMods() => SelectedMods.Value;
protected virtual IEnumerable<ShearedButton> CreateFooterButtons()
{
if (AllowCustomisation)
@ -279,7 +289,7 @@ namespace osu.Game.Overlays.Mods
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
BeatmapInfo = { Value = beatmap?.BeatmapInfo }
BeatmapInfo = { Value = Beatmap?.BeatmapInfo },
},
}
});
@ -316,20 +326,26 @@ namespace osu.Game.Overlays.Mods
SelectedMods.BindValueChanged(_ =>
{
updateRankingInformation();
updateFromExternalSelection();
updateCustomisation();
ActiveMods.Value = ComputeActiveMods();
}, true);
ActiveMods.BindValueChanged(_ =>
{
updateOverlayInformation();
modSettingChangeTracker?.Dispose();
if (AllowCustomisation)
{
// Importantly, use SelectedMods.Value here (and not the ValueChanged NewValue) as the latter can
// Importantly, use ActiveMods.Value here (and not the ValueChanged NewValue) as the latter can
// potentially be stale, due to complexities in the way change trackers work.
//
// See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988
modSettingChangeTracker = new ModSettingChangeTracker(SelectedMods.Value);
modSettingChangeTracker.SettingChanged += _ => updateRankingInformation();
modSettingChangeTracker = new ModSettingChangeTracker(ActiveMods.Value);
modSettingChangeTracker.SettingChanged += _ => updateOverlayInformation();
}
}, true);
@ -454,18 +470,25 @@ namespace osu.Game.Overlays.Mods
modState.ValidForSelection.Value = modState.Mod.Type != ModType.System && modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod);
}
private void updateRankingInformation()
/// <summary>
/// Updates any information displayed on the overlay regarding the effects of the active mods.
/// This reads from <see cref="ActiveMods"/> instead of <see cref="SelectedMods"/>.
/// </summary>
private void updateOverlayInformation()
{
if (rankingInformationDisplay != null)
{
if (rankingInformationDisplay == null)
return;
double multiplier = 1.0;
foreach (var mod in SelectedMods.Value)
foreach (var mod in ActiveMods.Value)
multiplier *= mod.ScoreMultiplier;
rankingInformationDisplay.ModMultiplier.Value = multiplier;
rankingInformationDisplay.Ranked.Value = SelectedMods.Value.All(m => m.Ranked);
rankingInformationDisplay.Ranked.Value = ActiveMods.Value.All(m => m.Ranked);
}
if (beatmapAttributesDisplay != null)
beatmapAttributesDisplay.Mods.Value = ActiveMods.Value;
}
private void updateCustomisation()

View File

@ -86,7 +86,10 @@ namespace osu.Game.Overlays.Mods
{
modSettingsFlow.Clear();
foreach (var mod in SelectedMods.Value.AsOrdered())
// Importantly, the selected mods bindable is already ordered by the mod select overlay (following the order of mod columns and panels).
// Using AsOrdered produces a slightly different order (e.g. DT and NC no longer becoming adjacent),
// which breaks user expectations when interacting with the overlay.
foreach (var mod in SelectedMods.Value)
{
var settings = mod.CreateSettingsControls().ToList();
@ -110,10 +113,14 @@ namespace osu.Game.Overlays.Mods
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnHover(HoverEvent e) => true;
private partial class ModSettingsColumn : CompositeDrawable
public partial class ModSettingsColumn : CompositeDrawable
{
public readonly Mod Mod;
public ModSettingsColumn(Mod mod, IEnumerable<Drawable> settingsControls)
{
Mod = mod;
Width = 250;
RelativeSizeAxes = Axes.Y;
Padding = new MarginPadding { Bottom = 7 };

View File

@ -10,6 +10,7 @@ using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Statistics;
using osu.Game.Configuration;
using osu.Game.Localisation;
using osu.Game.Overlays.Notifications;
@ -107,6 +108,9 @@ namespace osu.Game.Overlays.Settings.Sections.General
try
{
GlobalStatistics.OutputToLog();
Logger.Flush();
var logStorage = Logger.Storage;
using (var outStream = storage.CreateFileSafely(archive_filename))

View File

@ -13,7 +13,6 @@ using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
@ -196,7 +195,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
var matrix = Matrix3.Identity;
MatrixExtensions.TranslateFromLeft(ref matrix, offset);
MatrixExtensions.RotateFromLeft(ref matrix, MathUtils.DegreesToRadians(rotation.Value));
MatrixExtensions.RotateFromLeft(ref matrix, float.DegreesToRadians(rotation.Value));
usableAreaQuad *= matrix;

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
@ -69,7 +68,7 @@ namespace osu.Game.Overlays.Settings
{
protected override bool AllowIme => false;
protected override bool CanAddCharacter(char character) => character.IsAsciiDigit();
protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character);
public new void NotifyInputError() => base.NotifyInputError();
}

View File

@ -235,7 +235,7 @@ namespace osu.Game.Overlays.Volume
Bindable.BindValueChanged(volume => { this.TransformTo(nameof(DisplayVolume), volume.NewValue, 400, Easing.OutQuint); }, true);
bgProgress.Current.Value = 0.75f;
bgProgress.Progress = 0.75f;
}
private int? displayVolumeInt;
@ -265,8 +265,8 @@ namespace osu.Game.Overlays.Volume
text.Text = intValue.ToString(CultureInfo.CurrentCulture);
}
volumeCircle.Current.Value = displayVolume * 0.75f;
volumeCircleGlow.Current.Value = displayVolume * 0.75f;
volumeCircle.Progress = displayVolume * 0.75f;
volumeCircleGlow.Progress = displayVolume * 0.75f;
if (intVolumeChanged && IsLoaded)
Scheduler.AddOnce(playTickSound);

View File

@ -11,3 +11,6 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("osu.Game.Tests.Dynamic")]
[assembly: InternalsVisibleTo("osu.Game.Tests.iOS")]
[assembly: InternalsVisibleTo("osu.Game.Tests.Android")]
// intended for Moq usage
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]

View File

@ -27,8 +27,7 @@ namespace osu.Game.Rulesets.Difficulty.Utils
public ReverseQueue(int initialCapacity)
{
if (initialCapacity <= 0)
throw new ArgumentOutOfRangeException(nameof(initialCapacity));
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(initialCapacity);
items = new T[initialCapacity];
capacity = initialCapacity;

View File

@ -40,8 +40,7 @@ namespace osu.Game.Rulesets.Objects.Types
public static PathType BSpline(int degree)
{
if (degree <= 0)
throw new ArgumentOutOfRangeException(nameof(degree), "The degree of a B-Spline path must be greater than zero.");
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(degree);
return new PathType { Type = SplineType.BSpline, Degree = degree };
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.Scoring
{
/// <summary>
@ -9,7 +11,7 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
public partial class AccumulatingHealthProcessor : HealthProcessor
{
protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value < requiredHealth;
protected override bool CheckDefaultFailCondition(JudgementResult _) => JudgedHits == MaxHits && Health.Value < requiredHealth;
private readonly double requiredHealth;

View File

@ -142,6 +142,14 @@ namespace osu.Game.Rulesets.Scoring
}
}
protected override bool CheckDefaultFailCondition(JudgementResult result)
{
if (result.Judgement.MaxResult.IsBonus() || result.Type == HitResult.IgnoreHit)
return false;
return base.CheckDefaultFailCondition(result);
}
protected override void Reset(bool storeResults)
{
base.Reset(storeResults);

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Scoring
public event Func<bool>? Failed;
/// <summary>
/// Additional conditions on top of <see cref="DefaultFailCondition"/> that cause a failing state.
/// Additional conditions on top of <see cref="CheckDefaultFailCondition"/> that cause a failing state.
/// </summary>
public event Func<HealthProcessor, JudgementResult, bool>? FailConditions;
@ -69,9 +69,10 @@ namespace osu.Game.Rulesets.Scoring
protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.HealthIncrease;
/// <summary>
/// The default conditions for failing.
/// Checks whether the default conditions for failing are met.
/// </summary>
protected virtual bool DefaultFailCondition => Precision.AlmostBigger(Health.MinValue, Health.Value);
/// <returns><see langword="true"/> if failure should be invoked.</returns>
protected virtual bool CheckDefaultFailCondition(JudgementResult result) => Precision.AlmostBigger(Health.MinValue, Health.Value);
/// <summary>
/// Whether the current state of <see cref="HealthProcessor"/> or the provided <paramref name="result"/> meets any fail condition.
@ -79,7 +80,7 @@ namespace osu.Game.Rulesets.Scoring
/// <param name="result">The judgement result.</param>
private bool meetsAnyFailCondition(JudgementResult result)
{
if (DefaultFailCondition)
if (CheckDefaultFailCondition(result))
return true;
if (FailConditions != null)

View File

@ -135,8 +135,7 @@ namespace osu.Game.Rulesets.UI
protected DrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
: base(ruleset)
{
if (beatmap == null)
throw new ArgumentNullException(nameof(beatmap), "Beatmap cannot be null.");
ArgumentNullException.ThrowIfNull(beatmap);
if (!(beatmap is Beatmap<TObject> tBeatmap))
throw new ArgumentException($"{GetType()} expected the beatmap to contain hitobjects of type {typeof(TObject)}.", nameof(beatmap));

View File

@ -189,7 +189,7 @@ namespace osu.Game.Rulesets.UI
double timeBehind = Math.Abs(proposedTime - referenceClock.CurrentTime);
isCatchingUp.Value = timeBehind > 200;
waitingOnFrames.Value = state == PlaybackState.NotValid;
waitingOnFrames.Value = hasReplayAttached && state == PlaybackState.NotValid;
manualClock.CurrentTime = proposedTime;
manualClock.Rate = Math.Abs(referenceClock.Rate) * direction;

View File

@ -42,8 +42,8 @@ namespace osu.Game.Scoring.Legacy
{
OnlineID = score.OnlineID,
Mods = score.APIMods,
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(),
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(),
ClientVersion = score.ClientVersion,
};
}

View File

@ -45,9 +45,10 @@ namespace osu.Game.Scoring.Legacy
/// </description></item>
/// <item><description>30000013: All local scores will use lazer definitions of ranks for consistency. Recalculates the rank of all scores.</description></item>
/// <item><description>30000014: Fix edge cases in conversion for osu! scores on selected beatmaps. Reconvert all scores.</description></item>
/// <item><description>30000015: Fix osu! standardised score estimation algorithm violating basic invariants. Reconvert all scores.</description></item>
/// </list>
/// </remarks>
public const int LATEST_VERSION = 30000014;
public const int LATEST_VERSION = 30000015;
/// <summary>
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.

View File

@ -198,10 +198,25 @@ namespace osu.Game.Scoring.Legacy
}
}
public static int? GetCountMiss(this ScoreInfo scoreInfo) =>
getCount(scoreInfo, HitResult.Miss);
public static int? GetCountMiss(this ScoreInfo scoreInfo)
{
switch (scoreInfo.Ruleset.OnlineID)
{
case 0:
case 1:
case 3:
return getCount(scoreInfo, HitResult.Miss);
case 2:
return (getCount(scoreInfo, HitResult.Miss) ?? 0) + (getCount(scoreInfo, HitResult.LargeTickMiss) ?? 0);
}
return null;
}
public static void SetCountMiss(this ScoreInfo scoreInfo, int value) =>
// this does not match the implementation of `GetCountMiss()` for catch,
// but we physically cannot recover that data anymore at this point.
scoreInfo.Statistics[HitResult.Miss] = value;
private static int? getCount(ScoreInfo scoreInfo, HitResult result)

View File

@ -9,6 +9,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components.Menus
{
@ -21,7 +22,7 @@ namespace osu.Game.Screens.Edit.Components.Menus
TabContainer.RelativeSizeAxes &= ~Axes.X;
TabContainer.AutoSizeAxes = Axes.X;
TabContainer.Padding = new MarginPadding(10);
TabContainer.Spacing = Vector2.Zero;
}
[BackgroundDependencyLoader]
@ -42,30 +43,51 @@ namespace osu.Game.Screens.Edit.Components.Menus
private partial class TabItem : OsuTabItem
{
private const float transition_length = 250;
private readonly Box background;
private Color4 backgroundIdleColour;
private Color4 backgroundHoverColour;
public TabItem(EditorScreenMode value)
: base(value)
{
Text.Margin = new MarginPadding();
Text.Margin = new MarginPadding(10);
Text.Anchor = Anchor.CentreLeft;
Text.Origin = Anchor.CentreLeft;
Text.Font = OsuFont.TorusAlternate;
Add(background = new Box
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue,
});
Bar.Expire();
}
protected override void OnActivated()
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
base.OnActivated();
Bar.ScaleTo(new Vector2(1, 5), transition_length, Easing.OutQuint);
backgroundIdleColour = colourProvider.Background2;
backgroundHoverColour = colourProvider.Background1;
}
protected override void OnDeactivated()
protected override void LoadComplete()
{
base.OnDeactivated();
Bar.ScaleTo(Vector2.One, transition_length, Easing.OutQuint);
base.LoadComplete();
background.Colour = backgroundIdleColour;
}
protected override void FadeHovered()
{
base.FadeHovered();
background.FadeColour(backgroundHoverColour, TRANSITION_LENGTH, Easing.OutQuint);
}
protected override void FadeUnhovered()
{
base.FadeUnhovered();
background.FadeColour(backgroundIdleColour, TRANSITION_LENGTH, Easing.OutQuint);
}
}
}

View File

@ -140,7 +140,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
Colour = this.baseColour = baseColour;
Current.Value = 1;
Progress = 1;
}
protected override void Update()

View File

@ -6,6 +6,7 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@ -265,6 +266,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return !Precision.AlmostIntersects(maskingBounds, rect);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (skin.IsNotNull())
skin.SourceChanged -= updateColour;
}
private partial class Tick : Circle
{
public Tick()

View File

@ -366,7 +366,7 @@ namespace osu.Game.Screens.Edit.Timing
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Current = { Value = 1f / light_count - angular_light_gap },
Progress = 1f / light_count - angular_light_gap,
Colour = colourProvider.Background2,
},
fillContent = new Container
@ -379,7 +379,7 @@ namespace osu.Game.Screens.Edit.Timing
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Current = { Value = 1f / light_count - angular_light_gap },
Progress = 1f / light_count - angular_light_gap,
Blending = BlendingParameters.Additive
},
// Please do not try and make sense of this.
@ -388,7 +388,7 @@ namespace osu.Game.Screens.Edit.Timing
Glow = new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Current = { Value = 1f / light_count - 0.01f },
Progress = 1f / light_count - 0.01f,
Blending = BlendingParameters.Additive
}.WithEffect(new GlowEffect
{

View File

@ -14,7 +14,6 @@ using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osuTK;
using osuTK.Graphics;
@ -209,13 +208,13 @@ namespace osu.Game.Screens.Menu
if (audioData[i] < amplitude_dead_zone)
continue;
float rotation = MathUtils.DegreesToRadians(i / (float)bars_per_visualiser * 360 + j * 360 / visualiser_rounds);
float rotation = float.DegreesToRadians(i / (float)bars_per_visualiser * 360 + j * 360 / visualiser_rounds);
float rotationCos = MathF.Cos(rotation);
float rotationSin = MathF.Sin(rotation);
// taking the cos and sin to the 0..1 range
var barPosition = new Vector2(rotationCos / 2 + 0.5f, rotationSin / 2 + 0.5f) * size;
var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(MathUtils.DegreesToRadians(360f / bars_per_visualiser)))) / 2f, bar_length * audioData[i]);
var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(float.DegreesToRadians(360f / bars_per_visualiser)))) / 2f, bar_length * audioData[i]);
// The distance between the position and the sides of the bar.
var bottomOffset = new Vector2(-rotationSin * barSize.X / 2, rotationCos * barSize.X / 2);
// The distance between the bottom side of the bar and the top side.

View File

@ -0,0 +1,53 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Screens.OnlinePlay.Match
{
public partial class RoomModSelectOverlay : UserModSelectOverlay
{
[Resolved]
private IBindable<PlaylistItem> selectedItem { get; set; } = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
private readonly List<Mod> roomRequiredMods = new List<Mod>();
public RoomModSelectOverlay()
: base(OverlayColourScheme.Plum)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedItem.BindValueChanged(v =>
{
roomRequiredMods.Clear();
if (v.NewValue is PlaylistItem item)
{
var rulesetInstance = rulesets.GetRuleset(item.RulesetID)?.CreateInstance();
Debug.Assert(rulesetInstance != null);
roomRequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance)));
}
ActiveMods.Value = ComputeActiveMods();
}, true);
}
protected override IReadOnlyList<Mod> ComputeActiveMods() => roomRequiredMods.Concat(base.ComputeActiveMods()).ToList();
}
}

View File

@ -241,7 +241,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
}
};
LoadComponent(UserModsSelectOverlay = new UserModSelectOverlay(OverlayColourScheme.Plum)
LoadComponent(UserModsSelectOverlay = new RoomModSelectOverlay
{
SelectedMods = { BindTarget = UserMods },
IsValidMod = _ => false

View File

@ -122,8 +122,17 @@ namespace osu.Game.Screens.Play
StopGameplayClock();
}
protected virtual void StartGameplayClock() => GameplayClock.Start();
protected virtual void StopGameplayClock() => GameplayClock.Stop();
protected virtual void StartGameplayClock()
{
Logger.Log($"{nameof(GameplayClockContainer)} started via call to {nameof(StartGameplayClock)}");
GameplayClock.Start();
}
protected virtual void StopGameplayClock()
{
Logger.Log($"{nameof(GameplayClockContainer)} stopped via call to {nameof(StopGameplayClock)}");
GameplayClock.Stop();
}
/// <summary>
/// Resets this <see cref="GameplayClockContainer"/> and the source to an initial state ready for gameplay.

View File

@ -198,9 +198,14 @@ namespace osu.Game.Screens.Play.HUD
bind();
}
protected override void Update()
{
base.Update();
circularProgress.Progress = Progress.Value;
}
private void bind()
{
((IBindable<double>)circularProgress.Current).BindTo(Progress);
Progress.ValueChanged += progress =>
{
icon.Scale = new Vector2(1 + (float)progress.NewValue * 0.2f);

View File

@ -31,6 +31,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
/// </summary>
public partial class AccuracyCircle : CompositeDrawable
{
/// <summary>
/// The total duration of the animation.
/// </summary>
public const double TOTAL_DURATION = APPEAR_DURATION + ACCURACY_TRANSFORM_DELAY + ACCURACY_TRANSFORM_DURATION;
/// <summary>
/// Duration for the transforms causing this component to appear.
/// </summary>
@ -147,7 +152,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
Colour = OsuColour.Gray(47),
Alpha = 0.5f,
InnerRadius = accuracy_circle_radius + 0.01f, // Extends a little bit into the circle
Current = { Value = 1 },
Progress = 1,
},
accuracyCircle = new CircularProgress
{
@ -189,11 +194,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
rankText = new RankText(score.Rank)
};
if (withFlair)
{
if (isFailedSDueToMisses)
AddInternal(failedSRankText = new RankText(ScoreRank.S));
if (withFlair)
{
var applauseSamples = new List<string> { applauseSampleName };
if (score.Rank >= ScoreRank.B)
// when rank is B or higher, play legacy applause sample on legacy skins.
@ -268,7 +273,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
if (targetAccuracy < 1 && targetAccuracy >= visual_alignment_offset)
targetAccuracy -= visual_alignment_offset;
accuracyCircle.FillTo(targetAccuracy, ACCURACY_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING);
accuracyCircle.ProgressTo(targetAccuracy, ACCURACY_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING);
if (withFlair)
{
@ -321,8 +326,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
{
rankText.Appear();
if (!withFlair) return;
if (withFlair)
{
Schedule(() =>
{
isTicking = false;
@ -341,6 +346,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
});
}
}
}
if (isFailedSDueToMisses)
{
@ -359,7 +365,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
.FadeOut(800, Easing.Out);
accuracyCircle
.FillTo(accuracyS - GRADE_SPACING_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint);
.ProgressTo(accuracyS - GRADE_SPACING_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint);
badges.Single(b => b.Rank == getRank(ScoreRank.S))
.FadeOut(70, Easing.OutQuint);

View File

@ -67,7 +67,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
{
public double RevealProgress
{
set => Current.Value = Math.Clamp(value, startProgress, endProgress) - startProgress;
set => Progress = Math.Clamp(value, startProgress, endProgress) - startProgress;
}
private readonly double startProgress;

View File

@ -25,8 +25,10 @@ using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Placeholders;
using osu.Game.Overlays;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking.Expanded.Accuracy;
using osu.Game.Screens.Ranking.Statistics;
using osuTK;
@ -41,6 +43,8 @@ namespace osu.Game.Screens.Ranking
public override bool? AllowGlobalTrackControl => true;
protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered;
public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>();
[CanBeNull]
@ -172,6 +176,10 @@ namespace osu.Game.Screens.Ranking
bool shouldFlair = player != null && !Score.User.IsBot;
ScorePanelList.AddScore(Score, shouldFlair);
// this is mostly for medal display.
// we don't want the medal animation to trample on the results screen animation, so we (ab)use `OverlayActivationMode`
// to give the results screen enough time to play the animation out before the medals can be shown.
Scheduler.AddDelayed(() => OverlayActivationMode.Value = OverlayActivation.All, shouldFlair ? AccuracyCircle.TOTAL_DURATION + 1000 : 0);
}
if (AllowWatchingReplay)

View File

@ -33,8 +33,7 @@ namespace osu.Game.Screens.Ranking.Statistics
/// <param name="items">The <see cref="SimpleStatisticItem"/>s to display in this row.</param>
public SimpleStatisticTable(int columnCount, [ItemNotNull] IEnumerable<SimpleStatisticItem> items)
{
if (columnCount < 1)
throw new ArgumentOutOfRangeException(nameof(columnCount));
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(columnCount);
this.columnCount = columnCount;
this.items = items.ToArray();

View File

@ -72,14 +72,14 @@ namespace osu.Game.Skinning
circularProgress.Scale = new Vector2(-1, 1);
circularProgress.Anchor = Anchor.TopRight;
circularProgress.Colour = new Colour4(199, 255, 47, 153);
circularProgress.Current.Value = 1 - progress;
circularProgress.Progress = 1 - progress;
}
else
{
circularProgress.Scale = new Vector2(1);
circularProgress.Anchor = Anchor.TopLeft;
circularProgress.Colour = new Colour4(255, 255, 255, 153);
circularProgress.Current.Value = progress;
circularProgress.Progress = progress;
}
}
}

View File

@ -73,7 +73,7 @@ namespace osu.Game.Skinning
private TextureUpload convertToGrayscale(TextureUpload textureUpload)
{
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
var image = Image.LoadPixelData(textureUpload.Data, textureUpload.Width, textureUpload.Height);
// stable uses `0.299 * r + 0.587 * g + 0.114 * b`
// (https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/Graphics/Textures/pTexture.cs#L138-L153)

View File

@ -61,7 +61,7 @@ namespace osu.Game.Skinning
if (textureUpload.Height > max_supported_texture_size || textureUpload.Width > max_supported_texture_size)
{
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
var image = Image.LoadPixelData(textureUpload.Data, textureUpload.Width, textureUpload.Height);
// The original texture upload will no longer be returned or used.
textureUpload.Dispose();

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics;
@ -46,32 +45,10 @@ namespace osu.Game.Storyboards
}
[JsonIgnore]
public double CommandsStartTime
{
get
{
double min = double.MaxValue;
for (int i = 0; i < timelines.Length; i++)
min = Math.Min(min, timelines[i].StartTime);
return min;
}
}
public double CommandsStartTime => timelines.Min(static t => t.StartTime);
[JsonIgnore]
public double CommandsEndTime
{
get
{
double max = double.MinValue;
for (int i = 0; i < timelines.Length; i++)
max = Math.Max(max, timelines[i].EndTime);
return max;
}
}
public double CommandsEndTime => timelines.Max(static t => t.EndTime);
[JsonIgnore]
public double CommandsDuration => CommandsEndTime - CommandsStartTime;
@ -83,19 +60,7 @@ namespace osu.Game.Storyboards
public virtual double EndTime => CommandsEndTime;
[JsonIgnore]
public bool HasCommands
{
get
{
for (int i = 0; i < timelines.Length; i++)
{
if (timelines[i].HasCommands)
return true;
}
return false;
}
}
public bool HasCommands => timelines.Any(static t => t.HasCommands);
public virtual IEnumerable<CommandTimeline<T>.TypedCommand> GetCommands<T>(CommandTimelineSelector<T> timelineSelector, double offset = 0)
{

View File

@ -85,12 +85,23 @@ namespace osu.Game.Storyboards
{
get
{
double latestEndTime = TimelineGroup.EndTime;
double latestEndTime = double.MaxValue;
// Ignore the whole setup if there are loops. In theory they can be handled here too, however the logic will be overly complex.
if (loops.Count == 0)
{
// Take the minimum time of all the potential "death" reasons.
latestEndTime = calculateOptimisedEndTime(TimelineGroup);
}
// If the logic above fails to find anything or discarded by the fact that there are loops present, latestEndTime will be double.MaxValue
// and thus conservativeEndTime will be used.
double conservativeEndTime = TimelineGroup.EndTime;
foreach (var l in loops)
latestEndTime = Math.Max(latestEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations);
conservativeEndTime = Math.Max(conservativeEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations);
return latestEndTime;
return Math.Min(latestEndTime, conservativeEndTime);
}
}
@ -194,6 +205,47 @@ namespace osu.Game.Storyboards
return commands;
}
private static double calculateOptimisedEndTime(CommandTimelineGroup timelineGroup)
{
// Here we are starting from maximum value and trying to minimise the end time on each step.
// There are few solid guesses we can make using which sprite's end time can be minimised: alpha = 0, scale = 0, colour.a = 0.
double[] deathTimes =
{
double.MaxValue, // alpha
double.MaxValue, // colour alpha
double.MaxValue, // scale
double.MaxValue, // scale x
double.MaxValue, // scale y
};
// The loops below are following the same pattern.
// We could be using TimelineGroup.EndValue here, however it's possible to have multiple commands with 0 value in a row
// so we are saving the earliest of them.
foreach (var alphaCommand in timelineGroup.Alpha.Commands)
{
if (alphaCommand.EndValue == 0)
// commands are ordered by the start time, however end time may vary. Save the earliest.
deathTimes[0] = Math.Min(alphaCommand.EndTime, deathTimes[0]);
else
// If value isn't 0 (sprite becomes visible again), revert the saved state.
deathTimes[0] = double.MaxValue;
}
foreach (var colourCommand in timelineGroup.Colour.Commands)
deathTimes[1] = colourCommand.EndValue.A == 0 ? Math.Min(colourCommand.EndTime, deathTimes[1]) : double.MaxValue;
foreach (var scaleCommand in timelineGroup.Scale.Commands)
deathTimes[2] = scaleCommand.EndValue == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[2]) : double.MaxValue;
foreach (var scaleCommand in timelineGroup.VectorScale.Commands)
{
deathTimes[3] = scaleCommand.EndValue.X == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[3]) : double.MaxValue;
deathTimes[4] = scaleCommand.EndValue.Y == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[4]) : double.MaxValue;
}
return deathTimes.Min();
}
public override string ToString()
=> $"{Path}, {Origin}, {InitialPosition}";

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
@ -28,8 +27,8 @@ namespace osu.Game.Utils
point.Y -= origin.Y;
Vector2 ret;
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
ret.X = point.X * MathF.Cos(float.DegreesToRadians(angle)) + point.Y * MathF.Sin(float.DegreesToRadians(angle));
ret.Y = point.X * -MathF.Sin(float.DegreesToRadians(angle)) + point.Y * MathF.Cos(float.DegreesToRadians(angle));
ret.X += origin.X;
ret.Y += origin.Y;

View File

@ -35,8 +35,7 @@ namespace osu.Game.Utils
/// <param name="capacity">The number of items the queue can hold.</param>
public LimitedCapacityQueue(int capacity)
{
if (capacity < 0)
throw new ArgumentOutOfRangeException(nameof(capacity));
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
this.capacity = capacity;
array = new T[capacity];

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