1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-14 18:32:56 +08:00

Merge branch 'master' into extract-random-mod-logic-2

This commit is contained in:
Dan Balasescu 2022-03-25 14:25:43 +09:00
commit cd5907f8da
212 changed files with 5593 additions and 1784 deletions

View File

@ -27,7 +27,7 @@
] ]
}, },
"ppy.localisationanalyser.tools": { "ppy.localisationanalyser.tools": {
"version": "2021.1210.0", "version": "2022.320.0",
"commands": [ "commands": [
"localisation" "localisation"
] ]

72
.github/ISSUE_TEMPLATE/bug-issue.yml vendored Normal file
View File

@ -0,0 +1,72 @@
name: Bug report
description: Report a very clearly broken issue.
body:
- type: markdown
attributes:
value: |
# osu! bug report
Important to note that your issue may have already been reported before. Please check:
- Pinned issues, at the top of https://github.com/ppy/osu/issues.
- Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0).
- And most importantly, search for your issue. If you find that it already exists, respond with a reaction or add any further information that may be helpful.
- type: dropdown
attributes:
label: Type
options:
- Crash to desktop
- Game behaviour
- Performance
- Cosmetic
- Other
validations:
required: true
- type: textarea
attributes:
label: Bug description
description: How did you find the bug? Any additional details that might help?
validations:
required: true
- type: textarea
attributes:
label: Screenshots or videos
description: Add screenshots or videos that show the bug here.
placeholder: Drag and drop the screenshots/videos into this box.
validations:
required: false
- type: input
attributes:
label: Version
description: The version you encountered this bug on. This is shown at the bottom of the main menu and also at the end of the settings screen.
validations:
required: true
- type: markdown
attributes:
value: |
## Logs
Attaching log files is required for every reported bug. See instructions below on how to find them.
If the game has not yet been closed since you found the bug:
1. Head on to game settings and click on "Open osu! folder"
2. Then open the `logs` folder located there
**Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead.
The default places to find the logs are as follows:
- `%AppData%/osu/logs` *on Windows*
- `~/.local/share/osu/logs` *on Linux & macOS*
- `Android/data/sh.ppy.osulazer/files/logs` *on Android*
- *On iOS*, they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
If you have selected a custom location for the game files, you can find the `logs` folder there.
After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below.
- type: textarea
attributes:
label: Logs
placeholder: Drag and drop the log files into this box.
validations:
required: true

5
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"recommendations": [
"ms-dotnettools.csharp"
]
}

View File

@ -31,7 +31,7 @@ If you are looking to install or test osu! without setting up a development envi
**Latest build:** **Latest build:**
| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.15+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) | [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| ------------- | ------------- | ------------- | ------------- | ------------- | | ------------- | ------------- | ------------- | ------------- | ------------- |
- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets. - The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.

View File

@ -1,11 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Pippidon.Scoring
{
public class PippidonScoreProcessor : ScoreProcessor
{
}
}

View File

@ -51,11 +51,11 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.304.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.325.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
<PackageReference Include="Realm" Version="10.9.0" /> <PackageReference Include="Realm" Version="10.10.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -24,7 +24,7 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.8.15-pre" /> <PackageReference Include="Clowd.Squirrel" Version="2.8.28-pre" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" /> <PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" /> <PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="6.0.0" /> <PackageReference Include="System.IO.Packaging" Version="6.0.0" />

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
{ {
scoreProcessor = new ScoreProcessor(); scoreProcessor = new ScoreProcessor(new CatchRuleset());
SetContents(_ => new CatchComboDisplay SetContents(_ => new CatchComboDisplay
{ {

View File

@ -19,7 +19,6 @@ using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Catch.Scoring; using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System; using System;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Rulesets.Catch.Edit; using osu.Game.Rulesets.Catch.Edit;
@ -182,7 +181,7 @@ namespace osu.Game.Rulesets.Catch
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin); public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score); public override PerformanceCalculator CreatePerformanceCalculator() => new CatchPerformanceCalculator();
public int LegacyID => 2; public int LegacyID => 2;

View File

@ -13,33 +13,29 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{ {
public class CatchPerformanceCalculator : PerformanceCalculator public class CatchPerformanceCalculator : PerformanceCalculator
{ {
protected new CatchDifficultyAttributes Attributes => (CatchDifficultyAttributes)base.Attributes;
private Mod[] mods;
private int fruitsHit; private int fruitsHit;
private int ticksHit; private int ticksHit;
private int tinyTicksHit; private int tinyTicksHit;
private int tinyTicksMissed; private int tinyTicksMissed;
private int misses; private int misses;
public CatchPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) public CatchPerformanceCalculator()
: base(ruleset, attributes, score) : base(new CatchRuleset())
{ {
} }
public override PerformanceAttributes Calculate() protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
{ {
mods = Score.Mods; var catchAttributes = (CatchDifficultyAttributes)attributes;
fruitsHit = Score.Statistics.GetValueOrDefault(HitResult.Great); fruitsHit = score.Statistics.GetValueOrDefault(HitResult.Great);
ticksHit = Score.Statistics.GetValueOrDefault(HitResult.LargeTickHit); ticksHit = score.Statistics.GetValueOrDefault(HitResult.LargeTickHit);
tinyTicksHit = Score.Statistics.GetValueOrDefault(HitResult.SmallTickHit); tinyTicksHit = score.Statistics.GetValueOrDefault(HitResult.SmallTickHit);
tinyTicksMissed = Score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss); tinyTicksMissed = score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
misses = Score.Statistics.GetValueOrDefault(HitResult.Miss); misses = score.Statistics.GetValueOrDefault(HitResult.Miss);
// We are heavily relying on aim in catch the beat // We are heavily relying on aim in catch the beat
double value = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0; double value = Math.Pow(5.0 * Math.Max(1.0, catchAttributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
// Longer maps are worth more. "Longer" means how many hits there are which can contribute to combo // Longer maps are worth more. "Longer" means how many hits there are which can contribute to combo
int numTotalHits = totalComboHits(); int numTotalHits = totalComboHits();
@ -52,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
value *= Math.Pow(0.97, misses); value *= Math.Pow(0.97, misses);
// Combo scaling // Combo scaling
if (Attributes.MaxCombo > 0) if (catchAttributes.MaxCombo > 0)
value *= Math.Min(Math.Pow(Score.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0);
double approachRate = Attributes.ApproachRate; double approachRate = catchAttributes.ApproachRate;
double approachRateFactor = 1.0; double approachRateFactor = 1.0;
if (approachRate > 9.0) if (approachRate > 9.0)
approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9 approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9
@ -66,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
value *= approachRateFactor; value *= approachRateFactor;
if (mods.Any(m => m is ModHidden)) if (score.Mods.Any(m => m is ModHidden))
{ {
// Hiddens gives almost nothing on max approach rate, and more the lower it is // Hiddens gives almost nothing on max approach rate, and more the lower it is
if (approachRate <= 10.0) if (approachRate <= 10.0)
@ -75,12 +71,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty
value *= 1.01 + 0.04 * (11.0 - Math.Min(11.0, approachRate)); // 5% at AR 10, 1% at AR 11 value *= 1.01 + 0.04 * (11.0 - Math.Min(11.0, approachRate)); // 5% at AR 10, 1% at AR 11
} }
if (mods.Any(m => m is ModFlashlight)) if (score.Mods.Any(m => m is ModFlashlight))
value *= 1.35 * lengthBonus; value *= 1.35 * lengthBonus;
value *= Math.Pow(accuracy(), 5.5); value *= Math.Pow(accuracy(), 5.5);
if (mods.Any(m => m is ModNoFail)) if (score.Mods.Any(m => m is ModNoFail))
value *= 0.90; value *= 0.90;
return new CatchPerformanceAttributes return new CatchPerformanceAttributes

View File

@ -7,5 +7,11 @@ namespace osu.Game.Rulesets.Catch.Scoring
{ {
public class CatchScoreProcessor : ScoreProcessor public class CatchScoreProcessor : ScoreProcessor
{ {
public CatchScoreProcessor()
: base(new CatchRuleset())
{
}
protected override double ClassicScoreMultiplier => 28;
} }
} }

View File

@ -13,10 +13,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{ {
public class ManiaPerformanceCalculator : PerformanceCalculator public class ManiaPerformanceCalculator : PerformanceCalculator
{ {
protected new ManiaDifficultyAttributes Attributes => (ManiaDifficultyAttributes)base.Attributes;
private Mod[] mods;
// Score after being scaled by non-difficulty-increasing mods // Score after being scaled by non-difficulty-increasing mods
private double scaledScore; private double scaledScore;
@ -27,39 +23,40 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private int countMeh; private int countMeh;
private int countMiss; private int countMiss;
public ManiaPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) public ManiaPerformanceCalculator()
: base(ruleset, attributes, score) : base(new ManiaRuleset())
{ {
} }
public override PerformanceAttributes Calculate() protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
{ {
mods = Score.Mods; var maniaAttributes = (ManiaDifficultyAttributes)attributes;
scaledScore = Score.TotalScore;
countPerfect = Score.Statistics.GetValueOrDefault(HitResult.Perfect);
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
countGood = Score.Statistics.GetValueOrDefault(HitResult.Good);
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
if (Attributes.ScoreMultiplier > 0) scaledScore = score.TotalScore;
countPerfect = score.Statistics.GetValueOrDefault(HitResult.Perfect);
countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
countGood = score.Statistics.GetValueOrDefault(HitResult.Good);
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
if (maniaAttributes.ScoreMultiplier > 0)
{ {
// Scale score up, so it's comparable to other keymods // Scale score up, so it's comparable to other keymods
scaledScore *= 1.0 / Attributes.ScoreMultiplier; scaledScore *= 1.0 / maniaAttributes.ScoreMultiplier;
} }
// Arbitrary initial value for scaling pp in order to standardize distributions across game modes. // Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
// The specific number has no intrinsic meaning and can be adjusted as needed. // The specific number has no intrinsic meaning and can be adjusted as needed.
double multiplier = 0.8; double multiplier = 0.8;
if (mods.Any(m => m is ModNoFail)) if (score.Mods.Any(m => m is ModNoFail))
multiplier *= 0.9; multiplier *= 0.9;
if (mods.Any(m => m is ModEasy)) if (score.Mods.Any(m => m is ModEasy))
multiplier *= 0.5; multiplier *= 0.5;
double difficultyValue = computeDifficultyValue(); double difficultyValue = computeDifficultyValue(maniaAttributes);
double accValue = computeAccuracyValue(difficultyValue); double accValue = computeAccuracyValue(difficultyValue, maniaAttributes);
double totalValue = double totalValue =
Math.Pow( Math.Pow(
Math.Pow(difficultyValue, 1.1) + Math.Pow(difficultyValue, 1.1) +
@ -75,9 +72,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty
}; };
} }
private double computeDifficultyValue() private double computeDifficultyValue(ManiaDifficultyAttributes attributes)
{ {
double difficultyValue = Math.Pow(5 * Math.Max(1, Attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0; double difficultyValue = Math.Pow(5 * Math.Max(1, attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0;
difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
@ -97,14 +94,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return difficultyValue; return difficultyValue;
} }
private double computeAccuracyValue(double difficultyValue) private double computeAccuracyValue(double difficultyValue, ManiaDifficultyAttributes attributes)
{ {
if (Attributes.GreatHitWindow <= 0) if (attributes.GreatHitWindow <= 0)
return 0; return 0;
// Lots of arbitrary values from testing. // Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
double accuracyValue = Math.Max(0.0, 0.2 - (Attributes.GreatHitWindow - 34) * 0.006667) double accuracyValue = Math.Max(0.0, 0.2 - (attributes.GreatHitWindow - 34) * 0.006667)
* difficultyValue * difficultyValue
* Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1); * Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1);

View File

@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new ManiaPerformanceCalculator(this, attributes, score); public override PerformanceCalculator CreatePerformanceCalculator() => new ManiaPerformanceCalculator();
public const string SHORT_NAME = "mania"; public const string SHORT_NAME = "mania";

View File

@ -7,8 +7,15 @@ namespace osu.Game.Rulesets.Mania.Scoring
{ {
internal class ManiaScoreProcessor : ScoreProcessor internal class ManiaScoreProcessor : ScoreProcessor
{ {
public ManiaScoreProcessor()
: base(new ManiaRuleset())
{
}
protected override double DefaultAccuracyPortion => 0.99; protected override double DefaultAccuracyPortion => 0.99;
protected override double DefaultComboPortion => 0.01; protected override double DefaultComboPortion => 0.01;
protected override double ClassicScoreMultiplier => 16;
} }
} }

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -14,10 +13,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{ {
public class OsuPerformanceCalculator : PerformanceCalculator public class OsuPerformanceCalculator : PerformanceCalculator
{ {
public new OsuDifficultyAttributes Attributes => (OsuDifficultyAttributes)base.Attributes;
private Mod[] mods;
private double accuracy; private double accuracy;
private int scoreMaxCombo; private int scoreMaxCombo;
private int countGreat; private int countGreat;
@ -27,31 +22,32 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double effectiveMissCount; private double effectiveMissCount;
public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) public OsuPerformanceCalculator()
: base(ruleset, attributes, score) : base(new OsuRuleset())
{ {
} }
public override PerformanceAttributes Calculate() protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
{ {
mods = Score.Mods; var osuAttributes = (OsuDifficultyAttributes)attributes;
accuracy = Score.Accuracy;
scoreMaxCombo = Score.MaxCombo; accuracy = score.Accuracy;
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great); scoreMaxCombo = score.MaxCombo;
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok); countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
effectiveMissCount = calculateEffectiveMissCount(); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
if (mods.Any(m => m is OsuModNoFail)) if (score.Mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount); multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
if (mods.Any(m => m is OsuModSpunOut) && totalHits > 0) if (score.Mods.Any(m => m is OsuModSpunOut) && totalHits > 0)
multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); multiplier *= 1.0 - Math.Pow((double)osuAttributes.SpinnerCount / totalHits, 0.85);
if (mods.Any(h => h is OsuModRelax)) if (score.Mods.Any(h => h is OsuModRelax))
{ {
// As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it. // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits); effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits);
@ -59,10 +55,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
multiplier *= 0.6; multiplier *= 0.6;
} }
double aimValue = computeAimValue(); double aimValue = computeAimValue(score, osuAttributes);
double speedValue = computeSpeedValue(); double speedValue = computeSpeedValue(score, osuAttributes);
double accuracyValue = computeAccuracyValue(); double accuracyValue = computeAccuracyValue(score, osuAttributes);
double flashlightValue = computeFlashlightValue(); double flashlightValue = computeFlashlightValue(score, osuAttributes);
double totalValue = double totalValue =
Math.Pow( Math.Pow(
Math.Pow(aimValue, 1.1) + Math.Pow(aimValue, 1.1) +
@ -82,11 +78,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
}; };
} }
private double computeAimValue() private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{ {
double rawAim = Attributes.AimDifficulty; double rawAim = attributes.AimDifficulty;
if (mods.Any(m => m is OsuModTouchDevice)) if (score.Mods.Any(m => m is OsuModTouchDevice))
rawAim = Math.Pow(rawAim, 0.8); rawAim = Math.Pow(rawAim, 0.8);
double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0; double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
@ -99,44 +95,44 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (effectiveMissCount > 0) if (effectiveMissCount > 0)
aimValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount); aimValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount);
aimValue *= getComboScalingFactor(); aimValue *= getComboScalingFactor(attributes);
double approachRateFactor = 0.0; double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33) if (attributes.ApproachRate > 10.33)
approachRateFactor = 0.3 * (Attributes.ApproachRate - 10.33); approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
else if (Attributes.ApproachRate < 8.0) else if (attributes.ApproachRate < 8.0)
approachRateFactor = 0.1 * (8.0 - Attributes.ApproachRate); approachRateFactor = 0.1 * (8.0 - attributes.ApproachRate);
aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
if (mods.Any(m => m is OsuModBlinds)) if (score.Mods.Any(m => m is OsuModBlinds))
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate); aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate);
else if (mods.Any(h => h is OsuModHidden)) else if (score.Mods.Any(h => h is OsuModHidden))
{ {
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
} }
// We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator. // We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator.
double estimateDifficultSliders = Attributes.SliderCount * 0.15; double estimateDifficultSliders = attributes.SliderCount * 0.15;
if (Attributes.SliderCount > 0) if (attributes.SliderCount > 0)
{ {
double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, Attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders); double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
double sliderNerfFactor = (1 - Attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + Attributes.SliderFactor; double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor;
aimValue *= sliderNerfFactor; aimValue *= sliderNerfFactor;
} }
aimValue *= accuracy; aimValue *= accuracy;
// It is important to consider accuracy difficulty when scaling with accuracy. // It is important to consider accuracy difficulty when scaling with accuracy.
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
return aimValue; return aimValue;
} }
private double computeSpeedValue() private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{ {
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@ -146,27 +142,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (effectiveMissCount > 0) if (effectiveMissCount > 0)
speedValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); speedValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
speedValue *= getComboScalingFactor(); speedValue *= getComboScalingFactor(attributes);
double approachRateFactor = 0.0; double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33) if (attributes.ApproachRate > 10.33)
approachRateFactor = 0.3 * (Attributes.ApproachRate - 10.33); approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
if (mods.Any(m => m is OsuModBlinds)) if (score.Mods.Any(m => m is OsuModBlinds))
{ {
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given. // Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
speedValue *= 1.12; speedValue *= 1.12;
} }
else if (mods.Any(m => m is OsuModHidden)) else if (score.Mods.Any(m => m is OsuModHidden))
{ {
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
} }
// Scale the speed value with accuracy and OD. // Scale the speed value with accuracy and OD.
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2); speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping. // Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
@ -174,14 +170,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return speedValue; return speedValue;
} }
private double computeAccuracyValue() private double computeAccuracyValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{ {
if (mods.Any(h => h is OsuModRelax)) if (score.Mods.Any(h => h is OsuModRelax))
return 0.0; return 0.0;
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window. // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
double betterAccuracyPercentage; double betterAccuracyPercentage;
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount; int amountHitObjectsWithAccuracy = attributes.HitCircleCount;
if (amountHitObjectsWithAccuracy > 0) if (amountHitObjectsWithAccuracy > 0)
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
@ -194,43 +190,43 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Lots of arbitrary values from testing. // Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution. // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution.
double accuracyValue = Math.Pow(1.52163, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83; double accuracyValue = Math.Pow(1.52163, attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer. // Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)); accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
if (mods.Any(m => m is OsuModBlinds)) if (score.Mods.Any(m => m is OsuModBlinds))
accuracyValue *= 1.14; accuracyValue *= 1.14;
else if (mods.Any(m => m is OsuModHidden)) else if (score.Mods.Any(m => m is OsuModHidden))
accuracyValue *= 1.08; accuracyValue *= 1.08;
if (mods.Any(m => m is OsuModFlashlight)) if (score.Mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02; accuracyValue *= 1.02;
return accuracyValue; return accuracyValue;
} }
private double computeFlashlightValue() private double computeFlashlightValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{ {
if (!mods.Any(h => h is OsuModFlashlight)) if (!score.Mods.Any(h => h is OsuModFlashlight))
return 0.0; return 0.0;
double rawFlashlight = Attributes.FlashlightDifficulty; double rawFlashlight = attributes.FlashlightDifficulty;
if (mods.Any(m => m is OsuModTouchDevice)) if (score.Mods.Any(m => m is OsuModTouchDevice))
rawFlashlight = Math.Pow(rawFlashlight, 0.8); rawFlashlight = Math.Pow(rawFlashlight, 0.8);
double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0; double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;
if (mods.Any(h => h is OsuModHidden)) if (score.Mods.Any(h => h is OsuModHidden))
flashlightValue *= 1.3; flashlightValue *= 1.3;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0) if (effectiveMissCount > 0)
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
flashlightValue *= getComboScalingFactor(); flashlightValue *= getComboScalingFactor(attributes);
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius. // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) + flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
@ -239,19 +235,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Scale the flashlight value with accuracy _slightly_. // Scale the flashlight value with accuracy _slightly_.
flashlightValue *= 0.5 + accuracy / 2.0; flashlightValue *= 0.5 + accuracy / 2.0;
// It is important to also consider accuracy difficulty when doing that. // It is important to also consider accuracy difficulty when doing that.
flashlightValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
return flashlightValue; return flashlightValue;
} }
private double calculateEffectiveMissCount() private double calculateEffectiveMissCount(OsuDifficultyAttributes attributes)
{ {
// Guess the number of misses + slider breaks from combo // Guess the number of misses + slider breaks from combo
double comboBasedMissCount = 0.0; double comboBasedMissCount = 0.0;
if (Attributes.SliderCount > 0) if (attributes.SliderCount > 0)
{ {
double fullComboThreshold = Attributes.MaxCombo - 0.1 * Attributes.SliderCount; double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
if (scoreMaxCombo < fullComboThreshold) if (scoreMaxCombo < fullComboThreshold)
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo); comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
} }
@ -262,7 +258,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return Math.Max(countMiss, comboBasedMissCount); return Math.Max(countMiss, comboBasedMissCount);
} }
private double getComboScalingFactor() => Attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh; private int totalSuccessfulHits => countGreat + countOk + countMeh;
} }

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circle the circle chases you!"; public override string Description => "No need to chase the circle the circle chases you!";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) };
private IFrameStableClock gameplayClock; private IFrameStableClock gameplayClock;

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -16,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject> public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
{ {
public override Type[] IncompatibleMods => new[] { typeof(OsuModStrictTracking) };
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")] [SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true); public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
{ {
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModAimAssist) }).ToArray();
/// <summary> /// <summary>
/// How early before a hitobject's start time to trigger a hit. /// How early before a hitobject's start time to trigger a hit.

View File

@ -0,0 +1,148 @@
// 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 System.Threading;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModStrictTracking : Mod, IApplicableAfterBeatmapConversion, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => @"Strict Tracking";
public override string Acronym => @"ST";
public override IconUsage? Icon => FontAwesome.Solid.PenFancy;
public override ModType Type => ModType.DifficultyIncrease;
public override string Description => @"Follow circles just got serious...";
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModClassic) };
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
if (drawable is DrawableSlider slider)
{
slider.Tracking.ValueChanged += e =>
{
if (e.NewValue || slider.Judged) return;
var tail = slider.NestedHitObjects.OfType<StrictTrackingDrawableSliderTail>().First();
if (!tail.Judged)
tail.MissForcefully();
};
}
}
public void ApplyToBeatmap(IBeatmap beatmap)
{
var osuBeatmap = (OsuBeatmap)beatmap;
if (osuBeatmap.HitObjects.Count == 0) return;
var hitObjects = osuBeatmap.HitObjects.Select(ho =>
{
if (ho is Slider slider)
{
var newSlider = new StrictTrackingSlider(slider);
return newSlider;
}
return ho;
}).ToList();
osuBeatmap.HitObjects = hitObjects;
}
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
drawableRuleset.Playfield.RegisterPool<StrictTrackingSliderTailCircle, StrictTrackingDrawableSliderTail>(10, 100);
}
private class StrictTrackingSliderTailCircle : SliderTailCircle
{
public StrictTrackingSliderTailCircle(Slider slider)
: base(slider)
{
}
public override Judgement CreateJudgement() => new OsuJudgement();
}
private class StrictTrackingDrawableSliderTail : DrawableSliderTail
{
public override bool DisplayResult => true;
}
private class StrictTrackingSlider : Slider
{
public StrictTrackingSlider(Slider original)
{
StartTime = original.StartTime;
Samples = original.Samples;
Path = original.Path;
NodeSamples = original.NodeSamples;
RepeatCount = original.RepeatCount;
Position = original.Position;
NewCombo = original.NewCombo;
ComboOffset = original.ComboOffset;
LegacyLastTickOffset = original.LegacyLastTickOffset;
TickDistanceMultiplier = original.TickDistanceMultiplier;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken);
foreach (var e in sliderEvents)
{
switch (e.Type)
{
case SliderEventType.Head:
AddNested(HeadCircle = new SliderHeadCircle
{
StartTime = e.Time,
Position = Position,
StackHeight = StackHeight,
});
break;
case SliderEventType.LegacyLastTick:
AddNested(TailCircle = new StrictTrackingSliderTailCircle(this)
{
RepeatIndex = e.SpanIndex,
StartTime = e.Time,
Position = EndPosition,
StackHeight = StackHeight
});
break;
case SliderEventType.Repeat:
AddNested(new SliderRepeat(this)
{
RepeatIndex = e.SpanIndex,
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
});
break;
}
}
UpdateNestedSamples();
}
}
}
}

View File

@ -2,23 +2,22 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osuTK;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK.Graphics;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
@ -126,18 +125,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
} }
Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray(); Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
var slidingSamples = new List<ISampleInfo>();
var normalSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
if (normalSample != null)
slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(normalSample).With("sliderslide"));
var whistleSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE);
if (whistleSample != null)
slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(whistleSample).With("sliderwhistle"));
slidingSample.Samples = slidingSamples.ToArray();
} }
public override void StopAllSamples() public override void StopAllSamples()

View File

@ -74,6 +74,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
} }
protected override void LoadSamples()
{
// Tail models don't actually get samples, as the playback is handled by DrawableSlider.
// This override is only here for visibility in explaining this weird flow.
}
public override void PlaySamples()
{
// Tail models don't actually get samples, as the playback is handled by DrawableSlider.
// This override is only here for visibility in explaining this weird flow.
}
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()
{ {
base.UpdateInitialTransforms(); base.UpdateInitialTransforms();

View File

@ -121,15 +121,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
base.LoadSamples(); base.LoadSamples();
var firstSample = HitObject.Samples.FirstOrDefault(); spinningSample.Samples = HitObject.CreateSpinningSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
spinningSample.Frequency.Value = spinning_sample_initial_frequency;
if (firstSample != null)
{
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("spinnerspin");
spinningSample.Samples = new ISampleInfo[] { clone };
spinningSample.Frequency.Value = spinning_sample_initial_frequency;
}
} }
private void updateSpinningSample(ValueChangedEvent<bool> tracking) private void updateSpinningSample(ValueChangedEvent<bool> tracking)

View File

@ -29,6 +29,23 @@ namespace osu.Game.Rulesets.Osu.Objects
set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed. set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
} }
public override IList<HitSampleInfo> AuxiliarySamples => CreateSlidingSamples().Concat(TailSamples).ToArray();
public IList<HitSampleInfo> CreateSlidingSamples()
{
var slidingSamples = new List<HitSampleInfo>();
var normalSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
if (normalSample != null)
slidingSamples.Add(normalSample.With("sliderslide"));
var whistleSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE);
if (whistleSample != null)
slidingSamples.Add(whistleSample.With("sliderwhistle"));
return slidingSamples;
}
private readonly Cached<Vector2> endPositionCache = new Cached<Vector2>(); private readonly Cached<Vector2> endPositionCache = new Cached<Vector2>();
public override Vector2 EndPosition => endPositionCache.IsValid ? endPositionCache.Value : endPositionCache.Value = Position + this.CurvePositionAt(1); public override Vector2 EndPosition => endPositionCache.IsValid ? endPositionCache.Value : endPositionCache.Value = Position + this.CurvePositionAt(1);
@ -137,7 +154,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public Slider() public Slider()
{ {
SamplesBindable.CollectionChanged += (_, __) => updateNestedSamples(); SamplesBindable.CollectionChanged += (_, __) => UpdateNestedSamples();
Path.Version.ValueChanged += _ => updateNestedPositions(); Path.Version.ValueChanged += _ => updateNestedPositions();
} }
@ -210,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Objects
} }
} }
updateNestedSamples(); UpdateNestedSamples();
} }
private void updateNestedPositions() private void updateNestedPositions()
@ -224,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Objects
TailCircle.Position = EndPosition; TailCircle.Position = EndPosition;
} }
private void updateNestedSamples() protected void UpdateNestedSamples()
{ {
var firstSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL) var firstSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)
?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933) ?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)

View File

@ -1,7 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
@ -73,5 +77,20 @@ namespace osu.Game.Rulesets.Osu.Objects
public override Judgement CreateJudgement() => new OsuJudgement(); public override Judgement CreateJudgement() => new OsuJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override IList<HitSampleInfo> AuxiliarySamples => CreateSpinningSamples();
public HitSampleInfo[] CreateSpinningSamples()
{
var referenceSample = Samples.FirstOrDefault();
if (referenceSample == null)
return Array.Empty<HitSampleInfo>();
return new[]
{
SampleControlPoint.ApplyTo(referenceSample).With("spinnerspin")
};
}
} }
} }

View File

@ -159,6 +159,7 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()), new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()),
new OsuModHidden(), new OsuModHidden(),
new MultiMod(new OsuModFlashlight(), new OsuModBlinds()), new MultiMod(new OsuModFlashlight(), new OsuModBlinds()),
new OsuModStrictTracking()
}; };
case ModType.Conversion: case ModType.Conversion:
@ -213,7 +214,7 @@ namespace osu.Game.Rulesets.Osu
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(RulesetInfo, beatmap); public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(RulesetInfo, beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new OsuPerformanceCalculator(this, attributes, score); public override PerformanceCalculator CreatePerformanceCalculator() => new OsuPerformanceCalculator();
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this); public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);

View File

@ -11,6 +11,13 @@ namespace osu.Game.Rulesets.Osu.Scoring
{ {
public class OsuScoreProcessor : ScoreProcessor public class OsuScoreProcessor : ScoreProcessor
{ {
public OsuScoreProcessor()
: base(new OsuRuleset())
{
}
protected override double ClassicScoreMultiplier => 36;
protected override HitEvent CreateHitEvent(JudgementResult result) protected override HitEvent CreateHitEvent(JudgementResult result)
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit); => base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -37,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Child Child
.FadeTo(flash_opacity, EarlyActivationMilliseconds, Easing.OutQuint) .FadeTo(flash_opacity, EarlyActivationMilliseconds, Easing.OutQuint)
.Then() .Then()
.FadeOut(timingPoint.BeatLength - fade_length, Easing.OutSine); .FadeOut(Math.Max(fade_length, timingPoint.BeatLength - fade_length), Easing.OutSine);
} }
} }
} }

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (osuComponent.Component) switch (osuComponent.Component)
{ {
case OsuSkinComponents.FollowPoint: case OsuSkinComponents.FollowPoint:
return this.GetAnimation(component.LookupName, true, false, true, startAtCurrentTime: false); return this.GetAnimation(component.LookupName, true, true, true, startAtCurrentTime: false);
case OsuSkinComponents.SliderFollowCircle: case OsuSkinComponents.SliderFollowCircle:
var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true); var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);

View File

@ -14,37 +14,35 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{ {
public class TaikoPerformanceCalculator : PerformanceCalculator public class TaikoPerformanceCalculator : PerformanceCalculator
{ {
protected new TaikoDifficultyAttributes Attributes => (TaikoDifficultyAttributes)base.Attributes;
private Mod[] mods;
private int countGreat; private int countGreat;
private int countOk; private int countOk;
private int countMeh; private int countMeh;
private int countMiss; private int countMiss;
public TaikoPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) public TaikoPerformanceCalculator()
: base(ruleset, attributes, score) : base(new TaikoRuleset())
{ {
} }
public override PerformanceAttributes Calculate() protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
{ {
mods = Score.Mods; var taikoAttributes = (TaikoDifficultyAttributes)attributes;
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok); countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
if (mods.Any(m => m is ModNoFail)) if (score.Mods.Any(m => m is ModNoFail))
multiplier *= 0.90; multiplier *= 0.90;
if (mods.Any(m => m is ModHidden)) if (score.Mods.Any(m => m is ModHidden))
multiplier *= 1.10; multiplier *= 1.10;
double difficultyValue = computeDifficultyValue(); double difficultyValue = computeDifficultyValue(score, taikoAttributes);
double accuracyValue = computeAccuracyValue(); double accuracyValue = computeAccuracyValue(score, taikoAttributes);
double totalValue = double totalValue =
Math.Pow( Math.Pow(
Math.Pow(difficultyValue, 1.1) + Math.Pow(difficultyValue, 1.1) +
@ -59,30 +57,30 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
}; };
} }
private double computeDifficultyValue() private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
{ {
double difficultyValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0; double difficultyValue = Math.Pow(5.0 * Math.Max(1.0, attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0;
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
difficultyValue *= lengthBonus; difficultyValue *= lengthBonus;
difficultyValue *= Math.Pow(0.985, countMiss); difficultyValue *= Math.Pow(0.985, countMiss);
if (mods.Any(m => m is ModHidden)) if (score.Mods.Any(m => m is ModHidden))
difficultyValue *= 1.025; difficultyValue *= 1.025;
if (mods.Any(m => m is ModFlashlight<TaikoHitObject>)) if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
difficultyValue *= 1.05 * lengthBonus; difficultyValue *= 1.05 * lengthBonus;
return difficultyValue * Score.Accuracy; return difficultyValue * score.Accuracy;
} }
private double computeAccuracyValue() private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
{ {
if (Attributes.GreatHitWindow <= 0) if (attributes.GreatHitWindow <= 0)
return 0; return 0;
double accValue = Math.Pow(150.0 / Attributes.GreatHitWindow, 1.1) * Math.Pow(Score.Accuracy, 15) * 22.0; double accValue = Math.Pow(150.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 15) * 22.0;
// Bonus for many objects - it's harder to keep good accuracy up for longer // Bonus for many objects - it's harder to keep good accuracy up for longer
return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));

View File

@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{ {
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset; drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldAspect.Value = false;
} }
public void Update(Playfield playfield) public void Update(Playfield playfield)

View File

@ -7,8 +7,15 @@ namespace osu.Game.Rulesets.Taiko.Scoring
{ {
internal class TaikoScoreProcessor : ScoreProcessor internal class TaikoScoreProcessor : ScoreProcessor
{ {
public TaikoScoreProcessor()
: base(new TaikoRuleset())
{
}
protected override double DefaultAccuracyPortion => 0.75; protected override double DefaultAccuracyPortion => 0.75;
protected override double DefaultComboPortion => 0.25; protected override double DefaultComboPortion => 0.25;
protected override double ClassicScoreMultiplier => 22;
} }
} }

View File

@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Taiko
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(RulesetInfo, beatmap); public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(RulesetInfo, beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new TaikoPerformanceCalculator(this, attributes, score); public override PerformanceCalculator CreatePerformanceCalculator() => new TaikoPerformanceCalculator();
public int LegacyID => 1; public int LegacyID => 1;

View File

@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
public new BindableDouble TimeRange => base.TimeRange; public new BindableDouble TimeRange => base.TimeRange;
public readonly BindableBool LockPlayfieldAspect = new BindableBool(true);
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping; protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
protected override bool UserScrollSpeedAdjustment => false; protected override bool UserScrollSpeedAdjustment => false;
@ -70,7 +72,10 @@ namespace osu.Game.Rulesets.Taiko.UI
return ControlPoints[result]; return ControlPoints[result];
} }
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer(); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
{
LockPlayfieldAspect = { BindTarget = LockPlayfieldAspect }
};
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo); protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.UI
/// <summary> /// <summary>
/// Default height of a <see cref="TaikoPlayfield"/> when inside a <see cref="DrawableTaikoRuleset"/>. /// Default height of a <see cref="TaikoPlayfield"/> when inside a <see cref="DrawableTaikoRuleset"/>.
/// </summary> /// </summary>
public const float DEFAULT_HEIGHT = 212; public const float DEFAULT_HEIGHT = 200;
private Container<HitExplosion> hitExplosionContainer; private Container<HitExplosion> hitExplosionContainer;
private Container<KiaiHitExplosion> kiaiExplosionContainer; private Container<KiaiHitExplosion> kiaiExplosionContainer;

View File

@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Taiko.UI namespace osu.Game.Rulesets.Taiko.UI
{ {
@ -13,16 +13,22 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768; private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
private const float default_aspect = 16f / 9f; private const float default_aspect = 16f / 9f;
public readonly IBindable<bool> LockPlayfieldAspect = new BindableBool(true);
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
float aspectAdjust = Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect; float height = default_relative_height;
Size = new Vector2(1, default_relative_height * aspectAdjust);
if (LockPlayfieldAspect.Value)
height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
Height = height;
// Position the taiko playfield exactly one playfield from the top of the screen. // Position the taiko playfield exactly one playfield from the top of the screen.
RelativePositionAxes = Axes.Y; RelativePositionAxes = Axes.Y;
Y = Size.Y; Y = height;
} }
} }
} }

View File

@ -79,7 +79,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="DeepEqual" Version="2.0.0" /> <PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="System.Formats.Asn1"> <PackageReference Include="System.Formats.Asn1">
<Version>5.0.0</Version> <Version>5.0.0</Version>
</PackageReference> </PackageReference>

View File

@ -48,7 +48,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="DeepEqual" Version="2.0.0" /> <PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="Moq" Version="4.17.2" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
</Project> </Project>

View File

@ -8,6 +8,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch;
@ -64,6 +65,62 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
} }
[TestCase(3, true)]
[TestCase(6, false)]
[TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)]
public void TestLegacyBeatmapReplayOffsetsDecode(int beatmapVersion, bool offsetApplied)
{
const double first_frame_time = 48;
const double second_frame_time = 65;
var decoder = new TestLegacyScoreDecoder(beatmapVersion);
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
{
var score = decoder.Parse(resourceStream);
Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0)));
Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0)));
}
}
[TestCase(3)]
[TestCase(6)]
[TestCase(LegacyBeatmapDecoder.LATEST_VERSION)]
public void TestLegacyBeatmapReplayOffsetsEncodeDecode(int beatmapVersion)
{
const double first_frame_time = 2000;
const double second_frame_time = 3000;
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
var beatmap = new TestBeatmap(ruleset)
{
BeatmapInfo =
{
BeatmapVersion = beatmapVersion
}
};
var score = new Score
{
ScoreInfo = scoreInfo,
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
new OsuReplayFrame(first_frame_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
new OsuReplayFrame(second_frame_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
}
}
};
var decodedAfterEncode = encodeThenDecode(beatmapVersion, score, beatmap);
Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(first_frame_time));
Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time));
}
[Test] [Test]
public void TestCultureInvariance() public void TestCultureInvariance()
{ {
@ -86,15 +143,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
// rather than the classic ASCII U+002D HYPHEN-MINUS. // rather than the classic ASCII U+002D HYPHEN-MINUS.
CultureInfo.CurrentCulture = new CultureInfo("se"); CultureInfo.CurrentCulture = new CultureInfo("se");
var encodeStream = new MemoryStream(); var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
var encoder = new LegacyScoreEncoder(score, beatmap);
encoder.Encode(encodeStream);
var decodeStream = new MemoryStream(encodeStream.GetBuffer());
var decoder = new TestLegacyScoreDecoder();
var decodedAfterEncode = decoder.Parse(decodeStream);
Assert.Multiple(() => Assert.Multiple(() =>
{ {
@ -110,6 +159,20 @@ namespace osu.Game.Tests.Beatmaps.Formats
}); });
} }
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 TestLegacyScoreDecoder(beatmapVersion);
var decodedAfterEncode = decoder.Parse(decodeStream);
return decodedAfterEncode;
}
[TearDown] [TearDown]
public void TearDown() public void TearDown()
{ {
@ -118,6 +181,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
private class TestLegacyScoreDecoder : LegacyScoreDecoder private class TestLegacyScoreDecoder : LegacyScoreDecoder
{ {
private readonly int beatmapVersion;
private static readonly Dictionary<int, Ruleset> rulesets = new Ruleset[] private static readonly Dictionary<int, Ruleset> rulesets = new Ruleset[]
{ {
new OsuRuleset(), new OsuRuleset(),
@ -126,6 +191,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
new ManiaRuleset() new ManiaRuleset()
}.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID); }.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID);
public TestLegacyScoreDecoder(int beatmapVersion = LegacyBeatmapDecoder.LATEST_VERSION)
{
this.beatmapVersion = beatmapVersion;
}
protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId]; protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId];
protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap
@ -134,7 +204,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
{ {
MD5Hash = md5Hash, MD5Hash = md5Hash,
Ruleset = new OsuRuleset().RulesetInfo, Ruleset = new OsuRuleset().RulesetInfo,
Difficulty = new BeatmapDifficulty() Difficulty = new BeatmapDifficulty(),
BeatmapVersion = beatmapVersion,
} }
}); });
} }

View File

@ -147,7 +147,10 @@ namespace osu.Game.Tests.Database
Live<BeatmapSetInfo>? imported; Live<BeatmapSetInfo>? imported;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream())) using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
{
imported = await importer.Import(reader); imported = await importer.Import(reader);
EnsureLoaded(realm.Realm);
}
Assert.AreEqual(1, realm.Realm.All<BeatmapSetInfo>().Count()); Assert.AreEqual(1, realm.Realm.All<BeatmapSetInfo>().Count());
@ -510,6 +513,8 @@ namespace osu.Game.Tests.Database
new ImportTask(zipStream, string.Empty) new ImportTask(zipStream, string.Empty)
); );
realm.Run(r => r.Refresh());
checkBeatmapSetCount(realm.Realm, 0); checkBeatmapSetCount(realm.Realm, 0);
checkBeatmapCount(realm.Realm, 0); checkBeatmapCount(realm.Realm, 0);
@ -565,6 +570,8 @@ namespace osu.Game.Tests.Database
{ {
} }
EnsureLoaded(realm.Realm);
checkBeatmapSetCount(realm.Realm, 1); checkBeatmapSetCount(realm.Realm, 1);
checkBeatmapCount(realm.Realm, 12); checkBeatmapCount(realm.Realm, 12);
@ -590,6 +597,8 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.DeletePending); Assert.IsTrue(imported.DeletePending);
var originalAddedDate = imported.DateAdded;
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm); var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
@ -597,6 +606,7 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
Assert.IsFalse(imported.DeletePending); Assert.IsFalse(imported.DeletePending);
Assert.IsFalse(importedSecondTime.DeletePending); Assert.IsFalse(importedSecondTime.DeletePending);
Assert.That(importedSecondTime.DateAdded, Is.GreaterThan(originalAddedDate));
}); });
} }
@ -646,6 +656,8 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.DeletePending); Assert.IsTrue(imported.DeletePending);
var originalAddedDate = imported.DateAdded;
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm); var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
@ -653,6 +665,7 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
Assert.IsFalse(imported.DeletePending); Assert.IsFalse(imported.DeletePending);
Assert.IsFalse(importedSecondTime.DeletePending); Assert.IsFalse(importedSecondTime.DeletePending);
Assert.That(importedSecondTime.DateAdded, Is.GreaterThan(originalAddedDate));
}); });
} }
@ -720,6 +733,8 @@ namespace osu.Game.Tests.Database
var imported = importer.Import(toImport); var imported = importer.Import(toImport);
realm.Run(r => r.Refresh());
Assert.NotNull(imported); Assert.NotNull(imported);
Debug.Assert(imported != null); Debug.Assert(imported != null);
@ -885,6 +900,8 @@ namespace osu.Game.Tests.Database
string? temp = TestResources.GetTestBeatmapForImport(); string? temp = TestResources.GetTestBeatmapForImport();
await importer.Import(temp); await importer.Import(temp);
EnsureLoaded(realm.Realm);
// Update via the beatmap, not the beatmap info, to ensure correct linking // Update via the beatmap, not the beatmap info, to ensure correct linking
BeatmapSetInfo setToUpdate = realm.Realm.All<BeatmapSetInfo>().First(); BeatmapSetInfo setToUpdate = realm.Realm.All<BeatmapSetInfo>().First();

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Gameplay
{ {
var beatmap = new Beatmap<HitObject> { HitObjects = { new HitObject() } }; var beatmap = new Beatmap<HitObject> { HitObjects = { new HitObject() } };
var scoreProcessor = new ScoreProcessor(); var scoreProcessor = new ScoreProcessor(new OsuRuleset());
scoreProcessor.ApplyBeatmap(beatmap); scoreProcessor.ApplyBeatmap(beatmap);
// Apply a miss judgement // Apply a miss judgement
@ -39,7 +39,7 @@ namespace osu.Game.Tests.Gameplay
{ {
var beatmap = new Beatmap<HitObject> { HitObjects = { new HitObject() } }; var beatmap = new Beatmap<HitObject> { HitObjects = { new HitObject() } };
var scoreProcessor = new ScoreProcessor(); var scoreProcessor = new ScoreProcessor(new OsuRuleset());
scoreProcessor.ApplyBeatmap(beatmap); scoreProcessor.ApplyBeatmap(beatmap);
// Apply a judgement // Apply a judgement
@ -53,7 +53,7 @@ namespace osu.Game.Tests.Gameplay
{ {
var beatmap = new Beatmap<HitObject> { HitObjects = { new HitCircle() } }; var beatmap = new Beatmap<HitObject> { HitObjects = { new HitCircle() } };
var scoreProcessor = new ScoreProcessor(); var scoreProcessor = new ScoreProcessor(new OsuRuleset());
scoreProcessor.ApplyBeatmap(beatmap); scoreProcessor.ApplyBeatmap(beatmap);
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TestJudgement(HitResult.Great)) { Type = HitResult.Great }); scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TestJudgement(HitResult.Great)) { Type = HitResult.Great });

View File

@ -24,6 +24,20 @@ namespace osu.Game.Tests.Online
[TestFixture] [TestFixture]
public class TestAPIModJsonSerialization public class TestAPIModJsonSerialization
{ {
[Test]
public void TestUnknownMod()
{
var apiMod = new APIMod { Acronym = "WNG" };
var deserialized = JsonConvert.DeserializeObject<APIMod>(JsonConvert.SerializeObject(apiMod));
var converted = deserialized?.ToMod(new TestRuleset());
Assert.That(converted, Is.TypeOf(typeof(UnknownMod)));
Assert.That(converted?.Type, Is.EqualTo(ModType.System));
Assert.That(converted?.Acronym, Is.EqualTo("WNG??"));
}
[Test] [Test]
public void TestAcronymIsPreserved() public void TestAcronymIsPreserved()
{ {

View File

@ -118,7 +118,7 @@ namespace osu.Game.Tests.Online
[Test] [Test]
public void TestBeatmapDownloadingFlow() public void TestBeatmapDownloadingFlow()
{ {
AddAssert("ensure beatmap unavailable", () => !beatmaps.IsAvailableLocally(testBeatmapSet)); AddUntilStep("ensure beatmap unavailable", () => !beatmaps.IsAvailableLocally(testBeatmapSet));
addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded); addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded);
AddStep("start downloading", () => beatmapDownloader.Download(testBeatmapSet)); AddStep("start downloading", () => beatmapDownloader.Download(testBeatmapSet));
@ -132,7 +132,7 @@ namespace osu.Game.Tests.Online
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
AddUntilStep("wait for import", () => beatmaps.CurrentImport != null); AddUntilStep("wait for import", () => beatmaps.CurrentImport != null);
AddAssert("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet)); AddUntilStep("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet));
addAvailabilityCheckStep("state is locally available", BeatmapAvailability.LocallyAvailable); addAvailabilityCheckStep("state is locally available", BeatmapAvailability.LocallyAvailable);
} }

View File

@ -6,11 +6,15 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Rulesets.Scoring namespace osu.Game.Tests.Rulesets.Scoring
@ -23,7 +27,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
scoreProcessor = new ScoreProcessor(); scoreProcessor = new ScoreProcessor(new TestRuleset());
beatmap = new TestBeatmap(new RulesetInfo()) beatmap = new TestBeatmap(new RulesetInfo())
{ {
HitObjects = new List<HitObject> HitObjects = new List<HitObject>
@ -300,7 +304,26 @@ namespace osu.Game.Tests.Rulesets.Scoring
HitObjects = { new TestHitObject(result) } HitObjects = { new TestHitObject(result) }
}); });
Assert.That(scoreProcessor.GetImmediateScore(ScoringMode.Standardised, result.AffectsCombo() ? 1 : 0, statistic), Is.EqualTo(expectedScore).Within(0.5d)); Assert.That(scoreProcessor.ComputeFinalScore(ScoringMode.Standardised, new ScoreInfo
{
Ruleset = new TestRuleset().RulesetInfo,
MaxCombo = result.AffectsCombo() ? 1 : 0,
Statistics = statistic
}), Is.EqualTo(expectedScore).Within(0.5d));
}
private class TestRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new System.NotImplementedException();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new System.NotImplementedException();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new System.NotImplementedException();
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new System.NotImplementedException();
public override string Description => string.Empty;
public override string ShortName => string.Empty;
} }
private class TestJudgement : Judgement private class TestJudgement : Judgement

View File

@ -13,6 +13,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
@ -23,6 +24,7 @@ using osu.Game.Screens.Edit.Setup;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK; using osuTK;
using osuTK.Input;
using SharpCompress.Archives; using SharpCompress.Archives;
using SharpCompress.Archives.Zip; using SharpCompress.Archives.Zip;
@ -63,13 +65,19 @@ namespace osu.Game.Tests.Visual.Editing
EditorBeatmap editorBeatmap = null; EditorBeatmap editorBeatmap = null;
AddStep("store editor beatmap", () => editorBeatmap = EditorBeatmap); AddStep("store editor beatmap", () => editorBeatmap = EditorBeatmap);
AddStep("exit without save", () =>
AddStep("exit without save", () => Editor.Exit());
AddStep("hold to confirm", () =>
{ {
Editor.Exit(); var confirmButton = DialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogDangerousButton>().First();
DialogOverlay.CurrentDialog.PerformOkAction();
InputManager.MoveMouseTo(confirmButton);
InputManager.PressButton(MouseButton.Left);
}); });
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen()); AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.ID)?.Value.DeletePending == true); AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.ID)?.Value.DeletePending == true);
} }

View File

@ -17,6 +17,13 @@ namespace osu.Game.Tests.Visual.Editing
{ {
public class TestSceneEditorSaving : EditorSavingTestScene public class TestSceneEditorSaving : EditorSavingTestScene
{ {
[Test]
public void TestCantExitWithoutSaving()
{
AddRepeatStep("Exit", () => InputManager.Key(Key.Escape), 10);
AddAssert("Editor is still active screen", () => Game.ScreenStack.CurrentScreen is Editor);
}
[Test] [Test]
public void TestMetadata() public void TestMetadata()
{ {

View File

@ -18,9 +18,11 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
@ -32,11 +34,17 @@ namespace osu.Game.Tests.Visual.Gameplay
private SkinManager skinManager { get; set; } private SkinManager skinManager { get; set; }
[Cached] [Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(); private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
[Cached(typeof(HealthProcessor))] [Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
[Cached]
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
[Cached]
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
protected override bool HasCustomSteps => true; protected override bool HasCustomSteps => true;
[Test] [Test]

View File

@ -1,11 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -22,6 +25,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached] [Cached]
private Storyboard storyboard { get; set; } = new Storyboard(); private Storyboard storyboard { get; set; } = new Storyboard();
private IEnumerable<DrawableStoryboardSprite> sprites => this.ChildrenOfType<DrawableStoryboardSprite>();
[Test] [Test]
public void TestSkinSpriteDisallowedByDefault() public void TestSkinSpriteDisallowedByDefault()
{ {
@ -41,9 +46,54 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true);
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.Centre, Vector2.Zero))); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
assertSpritesFromSkin(true); assertSpritesFromSkin(true);
AddAssert("skinnable sprite has correct size", () => sprites.Any(s => Precision.AlmostEquals(s.ChildrenOfType<SkinnableSprite>().Single().Size, new Vector2(128, 128))));
}
[Test]
public void TestFlippedSprite()
{
const string lookup_name = "hitcircleoverlay";
AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true);
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
AddStep("flip sprites", () => sprites.ForEach(s =>
{
s.FlipH = true;
s.FlipV = true;
}));
AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight));
}
[Test]
public void TestNegativeScale()
{
const string lookup_name = "hitcircleoverlay";
AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true);
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(-1)));
AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight));
}
[Test]
public void TestNegativeScaleWithFlippedSprite()
{
const string lookup_name = "hitcircleoverlay";
AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true);
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(-1)));
AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight));
AddStep("flip sprites", () => sprites.ForEach(s =>
{
s.FlipH = true;
s.FlipV = true;
}));
AddAssert("origin back", () => sprites.All(s => s.Origin == Anchor.TopLeft));
} }
private DrawableStoryboardSprite createSprite(string lookupName, Anchor origin, Vector2 initialPosition) private DrawableStoryboardSprite createSprite(string lookupName, Anchor origin, Vector2 initialPosition)
@ -57,7 +107,6 @@ namespace osu.Game.Tests.Visual.Gameplay
private void assertSpritesFromSkin(bool fromSkin) => private void assertSpritesFromSkin(bool fromSkin) =>
AddAssert($"sprites are {(fromSkin ? "from skin" : "from storyboard")}", AddAssert($"sprites are {(fromSkin ? "from skin" : "from storyboard")}",
() => this.ChildrenOfType<DrawableStoryboardSprite>() () => sprites.All(sprite => sprite.ChildrenOfType<SkinnableSprite>().Any() == fromSkin));
.All(sprite => sprite.ChildrenOfType<SkinnableSprite>().Any() == fromSkin));
} }
} }

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1); AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1);
AddAssert("total number of results == 1", () => AddAssert("total number of results == 1", () =>
{ {
var score = new ScoreInfo(); var score = new ScoreInfo { Ruleset = Ruleset.Value };
((FailPlayer)Player).ScoreProcessor.PopulateScore(score); ((FailPlayer)Player).ScoreProcessor.PopulateScore(score);

View File

@ -8,11 +8,14 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -24,11 +27,17 @@ namespace osu.Game.Tests.Visual.Gameplay
private HUDOverlay hudOverlay; private HUDOverlay hudOverlay;
[Cached] [Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(); private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
[Cached(typeof(HealthProcessor))] [Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
[Cached]
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
[Cached]
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
// best way to check without exposing. // best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter; private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First(); private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First();

View File

@ -48,7 +48,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddStep("create display", () => recreateDisplay(new OsuHitWindows(), 5)); AddStep("create display", () => recreateDisplay(new OsuHitWindows(), 5));
AddRepeatStep("New random judgement", () => newJudgement(), 40); AddRepeatStep("New random judgement", () =>
{
double offset = RNG.Next(-150, 150);
newJudgement(offset, drawableRuleset.HitWindows.ResultFor(offset));
}, 400);
AddRepeatStep("New max negative", () => newJudgement(-drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20); AddRepeatStep("New max negative", () => newJudgement(-drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20);
AddRepeatStep("New max positive", () => newJudgement(drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20); AddRepeatStep("New max positive", () => newJudgement(drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20);
@ -273,6 +277,11 @@ namespace osu.Game.Tests.Visual.Gameplay
private class TestScoreProcessor : ScoreProcessor private class TestScoreProcessor : ScoreProcessor
{ {
public TestScoreProcessor()
: base(new OsuRuleset())
{
}
public void Reset() => base.Reset(false); public void Reset() => base.Reset(false);
} }
} }

View File

@ -5,6 +5,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Timing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -107,7 +108,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddStep("create player", () => AddStep("create player", () =>
{ {
Beatmap.Value = CreateWorkingBeatmap(beatmap, storyboard); Beatmap.Value = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), Audio);
LoadScreen(player = new LeadInPlayer()); LoadScreen(player = new LeadInPlayer());
}); });

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay
gameplayState = new GameplayState(beatmap, ruleset); gameplayState = new GameplayState(beatmap, ruleset);
gameplayState.LastJudgementResult.BindTo(lastJudgementResult); gameplayState.LastJudgementResult.BindTo(lastJudgementResult);
scoreProcessor = new ScoreProcessor(); scoreProcessor = new ScoreProcessor(ruleset);
Child = dependencyContainer = new DependencyProvidingContainer Child = dependencyContainer = new DependencyProvidingContainer
{ {

View File

@ -1,12 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning.Editor; using osu.Game.Skinning.Editor;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
@ -16,6 +23,9 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override bool Autoplay => true; protected override bool Autoplay => true;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[SetUpSteps] [SetUpSteps]
public override void SetUpSteps() public override void SetUpSteps()
{ {
@ -24,7 +34,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("reload skin editor", () => AddStep("reload skin editor", () =>
{ {
skinEditor?.Expire(); skinEditor?.Expire();
Player.ScaleTo(SkinEditorOverlay.VISIBLE_TARGET_SCALE); Player.ScaleTo(0.4f);
LoadComponentAsync(skinEditor = new SkinEditor(Player), Add); LoadComponentAsync(skinEditor = new SkinEditor(Player), Add);
}); });
} }
@ -35,6 +45,36 @@ namespace osu.Game.Tests.Visual.Gameplay
AddToggleStep("toggle editor visibility", visible => skinEditor.ToggleVisibility()); AddToggleStep("toggle editor visibility", visible => skinEditor.ToggleVisibility());
} }
[Test]
public void TestEditComponent()
{
BarHitErrorMeter hitErrorMeter = null;
AddStep("select bar hit error blueprint", () =>
{
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First(b => b.Item is BarHitErrorMeter);
hitErrorMeter = (BarHitErrorMeter)blueprint.Item;
skinEditor.SelectedComponents.Clear();
skinEditor.SelectedComponents.Add(blueprint.Item);
});
AddAssert("value is default", () => hitErrorMeter.JudgementLineThickness.IsDefault);
AddStep("hover first slider", () =>
{
InputManager.MoveMouseTo(
skinEditor.ChildrenOfType<SkinSettingsToolbox>().First()
.ChildrenOfType<SettingsSlider<float>>().First()
.ChildrenOfType<SliderBar<float>>().First()
);
});
AddStep("adjust slider via keyboard", () => InputManager.Key(Key.Left));
AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default);
}
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
} }
} }

View File

@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Skinning.Editor; using osu.Game.Skinning.Editor;
@ -11,13 +13,17 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneSkinEditorComponentsList : SkinnableTestScene public class TestSceneSkinEditorComponentsList : SkinnableTestScene
{ {
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[Test] [Test]
public void TestToggleEditor() public void TestToggleEditor()
{ {
AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox(300) AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopRight,
Origin = Anchor.TopCentre, Origin = Anchor.TopRight,
Width = 0.6f,
})); }));
} }

View File

@ -5,11 +5,13 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Skinning.Editor; using osu.Game.Skinning.Editor;
using osu.Game.Tests.Beatmaps;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -17,11 +19,17 @@ namespace osu.Game.Tests.Visual.Gameplay
public class TestSceneSkinEditorMultipleSkins : SkinnableTestScene public class TestSceneSkinEditorMultipleSkins : SkinnableTestScene
{ {
[Cached] [Cached]
private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(); private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
[Cached(typeof(HealthProcessor))] [Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
[Cached]
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
[Cached]
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -14,7 +15,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public class TestSceneSkinnableAccuracyCounter : SkinnableHUDComponentTestScene public class TestSceneSkinnableAccuracyCounter : SkinnableHUDComponentTestScene
{ {
[Cached] [Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(); private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
protected override Drawable CreateDefaultImplementation() => new DefaultAccuracyCounter(); protected override Drawable CreateDefaultImplementation() => new DefaultAccuracyCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyAccuracyCounter(); protected override Drawable CreateLegacyImplementation() => new LegacyAccuracyCounter();

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
@ -13,7 +14,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public class TestSceneSkinnableComboCounter : SkinnableHUDComponentTestScene public class TestSceneSkinnableComboCounter : SkinnableHUDComponentTestScene
{ {
[Cached] [Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(); private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
protected override Drawable CreateDefaultImplementation() => new DefaultComboCounter(); protected override Drawable CreateDefaultImplementation() => new DefaultComboCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyComboCounter(); protected override Drawable CreateLegacyImplementation() => new LegacyComboCounter();

View File

@ -10,11 +10,13 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -24,11 +26,17 @@ namespace osu.Game.Tests.Visual.Gameplay
private HUDOverlay hudOverlay; private HUDOverlay hudOverlay;
[Cached] [Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(); private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
[Cached(typeof(HealthProcessor))] [Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
[Cached]
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
[Cached]
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>(); private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();
// best way to check without exposing. // best way to check without exposing.

View File

@ -4,6 +4,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -13,7 +14,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public class TestSceneSkinnableScoreCounter : SkinnableHUDComponentTestScene public class TestSceneSkinnableScoreCounter : SkinnableHUDComponentTestScene
{ {
[Cached] [Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(); private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
protected override Drawable CreateDefaultImplementation() => new DefaultScoreCounter(); protected override Drawable CreateDefaultImplementation() => new DefaultScoreCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyScoreCounter(); protected override Drawable CreateLegacyImplementation() => new LegacyScoreCounter();

View File

@ -70,6 +70,56 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
} }
[Test]
public void TestSeekToGameplayStartFramesArriveAfterPlayerLoad()
{
const double gameplay_start = 10000;
loadSpectatingScreen();
start();
waitForPlayer();
sendFrames(startTime: gameplay_start);
AddAssert("time is greater than seek target", () => currentFrameStableTime > gameplay_start);
}
/// <summary>
/// Tests the same as <see cref="TestSeekToGameplayStartFramesArriveAfterPlayerLoad"/> but with the frames arriving just as <see cref="Player"/> is transitioning into existence.
/// </summary>
[Test]
public void TestSeekToGameplayStartFramesArriveAsPlayerLoaded()
{
const double gameplay_start = 10000;
loadSpectatingScreen();
start();
AddUntilStep("wait for player loader", () => (Stack.CurrentScreen as PlayerLoader)?.IsLoaded == true);
AddUntilStep("queue send frames on player load", () =>
{
var loadingPlayer = (Stack.CurrentScreen as PlayerLoader)?.CurrentPlayer;
if (loadingPlayer == null)
return false;
loadingPlayer.OnLoadComplete += _ =>
{
spectatorClient.SendFramesFromUser(streamingUser.Id, 10, gameplay_start);
};
return true;
});
waitForPlayer();
AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing);
AddAssert("time is greater than seek target", () => currentFrameStableTime > gameplay_start);
}
[Test] [Test]
public void TestFrameStarvationAndResume() public void TestFrameStarvationAndResume()
{ {
@ -319,9 +369,9 @@ namespace osu.Game.Tests.Visual.Gameplay
private void checkPaused(bool state) => private void checkPaused(bool state) =>
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state); AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
private void sendFrames(int count = 10) private void sendFrames(int count = 10, double startTime = 0)
{ {
AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count)); AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime));
} }
private void loadSpectatingScreen() private void loadSpectatingScreen()

View File

@ -0,0 +1,29 @@
// 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;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneUnknownMod : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
/// <summary>
/// This test also covers the scenario of exiting Player after an unsuccessful beatmap load.
/// </summary>
[Test]
public void TestUnknownModDoesntEnterGameplay()
{
CreateModTest(new ModTestData
{
Beatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo).Beatmap,
Mod = new UnknownMod("WNG"),
PassCondition = () => Player.IsLoaded && !Player.LoadedBeatmapSuccessfully
});
}
}
}

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -102,6 +103,11 @@ namespace osu.Game.Tests.Visual.Gameplay
private class TestScoreProcessor : ScoreProcessor private class TestScoreProcessor : ScoreProcessor
{ {
public TestScoreProcessor()
: base(new OsuRuleset())
{
}
public void Reset() => base.Reset(false); public void Reset() => base.Reset(false);
} }
} }

View File

@ -0,0 +1,338 @@
// 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.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMatchStartControl : MultiplayerTestScene
{
private MatchStartControl control;
private BeatmapSetInfo importedSet;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
private BeatmapManager beatmaps;
private RulesetStore rulesets;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}
[SetUp]
public new void Setup() => Schedule(() =>
{
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
};
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = control = new MatchStartControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
}
};
});
[Test]
public void TestStartWithCountdown()
{
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () =>
{
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
AddAssert("countdown button not visible", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad);
}
[Test]
public void TestCancelCountdown()
{
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () =>
{
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
}
[Test]
public void TestReadyAndUnReadyDuringCountdown()
{
AddStep("add second user as host", () =>
{
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.TransferHost(2);
});
AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(2) }).WaitSafely());
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
}
[Test]
public void TestCountdownButtonEnablementAndVisibilityWhileSpectating()
{
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddAssert("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }));
AddAssert("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready));
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
}
[Test]
public void TestSpectatingDuringCountdownWithNoReadyUsersCancelsCountdown()
{
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () =>
{
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
AddUntilStep("match not started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.Open);
}
[Test]
public void TestReadyButtonEnabledWhileSpectatingDuringCountdown()
{
AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }));
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready));
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () =>
{
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddAssert("ready button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value);
}
[Test]
public void TestBecomeHostDuringCountdownAndReady()
{
AddStep("add second user as host", () =>
{
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.TransferHost(2);
});
AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely());
AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null);
AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID));
AddUntilStep("local user is host", () => MultiplayerClient.Room?.Host?.Equals(MultiplayerClient.LocalUser) == true);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null);
}
[Test]
public void TestDeletedBeatmapDisableReady()
{
OsuButton readyButton = null;
AddUntilStep("ensure ready button enabled", () =>
{
readyButton = control.ChildrenOfType<OsuButton>().Single();
return readyButton.Enabled.Value;
});
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value);
AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value);
}
[Test]
public void TestToggleStateWhenNotHost()
{
AddStep("add second user as host", () =>
{
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.TransferHost(2);
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
}
[TestCase(true)]
[TestCase(false)]
public void TestToggleStateWhenHost(bool allReady)
{
AddStep("setup", () =>
{
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
if (!allReady)
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
verifyGameplayStartFlow();
}
[Test]
public void TestBecomeHostWhileReady()
{
AddStep("add host", () =>
{
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.TransferHost(2);
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0));
verifyGameplayStartFlow();
}
[Test]
public void TestLoseHostWhileReady()
{
AddStep("setup", () =>
{
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0));
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value);
}
[TestCase(true)]
[TestCase(false)]
public void TestManyUsersChangingState(bool isHost)
{
const int users = 10;
AddStep("setup", () =>
{
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
for (int i = 0; i < users; i++)
MultiplayerClient.AddUser(new APIUser { Id = i, Username = "Another user" });
});
if (!isHost)
AddStep("transfer host", () => MultiplayerClient.TransferHost(2));
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddRepeatStep("change user ready state", () =>
{
MultiplayerClient.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle);
}, 20);
AddRepeatStep("ready all users", () =>
{
var nextUnready = MultiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
if (nextUnready != null)
MultiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
}, users);
}
private void verifyGameplayStartFlow()
{
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
AddStep("finish gameplay", () =>
{
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded);
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay);
});
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value);
}
}
}

View File

@ -46,7 +46,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
var scoreProcessor = new OsuScoreProcessor(); var scoreProcessor = new OsuScoreProcessor();
scoreProcessor.ApplyBeatmap(playable); scoreProcessor.ApplyBeatmap(playable);
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()) { Expanded = { Value = true } }, Add); LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(Ruleset.Value, scoreProcessor, clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray())
{
Expanded = { Value = true }
}, Add);
}); });
AddUntilStep("wait for load", () => leaderboard.IsLoaded); AddUntilStep("wait for load", () => leaderboard.IsLoaded);

View File

@ -41,6 +41,7 @@ using osu.Game.Screens.Ranking;
using osu.Game.Screens.Spectate; using osu.Game.Screens.Spectate;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK.Input; using osuTK.Input;
using ReadyButton = osu.Game.Screens.OnlinePlay.Components.ReadyButton;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
@ -424,7 +425,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != multiplayerClient.Room?.Playlist.First().BeatmapID); AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != multiplayerClient.Room?.Playlist.First().BeatmapID);
AddStep("start match externally", () => multiplayerClient.StartMatch()); AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
@ -462,7 +463,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("Ruleset doesn't match current item", () => Ruleset.Value.OnlineID != multiplayerClient.Room?.Playlist.First().RulesetID); AddUntilStep("Ruleset doesn't match current item", () => Ruleset.Value.OnlineID != multiplayerClient.Room?.Playlist.First().RulesetID);
AddStep("start match externally", () => multiplayerClient.StartMatch()); AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
@ -500,7 +501,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
AddStep("start match externally", () => multiplayerClient.StartMatch()); AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
@ -535,7 +536,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("start match externally", () => multiplayerClient.StartMatch()); AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddAssert("play not started", () => multiplayerComponents.IsCurrentScreen()); AddAssert("play not started", () => multiplayerComponents.IsCurrentScreen());
} }
@ -568,7 +569,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("start match externally", () => multiplayerClient.StartMatch()); AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddStep("restore beatmap", () => AddStep("restore beatmap", () =>
{ {
@ -883,7 +884,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("start match by other user", () => AddStep("start match by other user", () =>
{ {
multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready); multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready);
multiplayerClient.StartMatch(); multiplayerClient.StartMatch().WaitSafely();
}); });
AddUntilStep("wait for loading", () => multiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad); AddUntilStep("wait for loading", () => multiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad);

View File

@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
scoreProcessor.ApplyBeatmap(playableBeatmap); scoreProcessor.ApplyBeatmap(playableBeatmap);
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray()) LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, multiplayerUsers.ToArray())
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
scoreProcessor.ApplyBeatmap(playableBeatmap); scoreProcessor.ApplyBeatmap(playableBeatmap);
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray()) LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, multiplayerUsers.ToArray())
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -11,6 +11,8 @@ using osu.Framework.Graphics;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -129,6 +131,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("item 1 not in lists", () => !inHistoryList(0) && !inQueueList(0)); AddUntilStep("item 1 not in lists", () => !inHistoryList(0) && !inQueueList(0));
} }
[Test]
public void TestQueueTabCount()
{
assertQueueTabCount(1);
addItemStep();
assertQueueTabCount(2);
addItemStep();
assertQueueTabCount(3);
AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely());
assertQueueTabCount(2);
AddStep("leave room", () => RoomManager.PartRoom());
AddUntilStep("wait for room part", () => !RoomJoined);
assertQueueTabCount(0);
}
[Ignore("Expired items are initially removed from the room.")] [Ignore("Expired items are initially removed from the room.")]
[Test] [Test]
public void TestJoinRoomWithMixedItemsAddedInCorrectLists() public void TestJoinRoomWithMixedItemsAddedInCorrectLists()
@ -213,6 +234,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
} }
private void assertQueueTabCount(int count)
{
string queueTabText = count > 0 ? $"Queue ({count})" : "Queue";
AddUntilStep($"Queue tab shows \"{queueTabText}\"", () =>
{
return this.ChildrenOfType<OsuTabControl<MultiplayerPlaylistDisplayMode>.OsuTabItem>()
.Single(t => t.Value == MultiplayerPlaylistDisplayMode.Queue)
.ChildrenOfType<OsuSpriteText>().Single().Text == queueTabText;
});
}
private void changeDisplayModeStep(MultiplayerPlaylistDisplayMode mode) => AddStep($"change list to {mode}", () => list.DisplayMode.Value = mode); private void changeDisplayModeStep(MultiplayerPlaylistDisplayMode mode) => AddStep($"change list to {mode}", () => list.DisplayMode.Value = mode);
private bool inQueueList(int playlistItemId) private bool inQueueList(int playlistItemId)

View File

@ -1,223 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerReadyButton : MultiplayerTestScene
{
private MultiplayerReadyButton button;
private BeatmapSetInfo importedSet;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private IDisposable readyClickOperation;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}
[SetUp]
public new void Setup() => Schedule(() =>
{
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
};
if (button != null)
Remove(button);
Add(button = new MultiplayerReadyButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
OnReadyClick = () =>
{
readyClickOperation = OngoingOperationTracker.BeginOperation();
Task.Run(async () =>
{
if (MultiplayerClient.IsHost && MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready)
{
await MultiplayerClient.StartMatch();
return;
}
await MultiplayerClient.ToggleReady();
readyClickOperation.Dispose();
});
}
});
});
[Test]
public void TestDeletedBeatmapDisableReady()
{
OsuButton readyButton = null;
AddUntilStep("ensure ready button enabled", () =>
{
readyButton = button.ChildrenOfType<OsuButton>().Single();
return readyButton.Enabled.Value;
});
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value);
AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value);
}
[Test]
public void TestToggleStateWhenNotHost()
{
AddStep("add second user as host", () =>
{
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.TransferHost(2);
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
}
[TestCase(true)]
[TestCase(false)]
public void TestToggleStateWhenHost(bool allReady)
{
AddStep("setup", () =>
{
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
if (!allReady)
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
verifyGameplayStartFlow();
}
[Test]
public void TestBecomeHostWhileReady()
{
AddStep("add host", () =>
{
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.TransferHost(2);
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0));
verifyGameplayStartFlow();
}
[Test]
public void TestLoseHostWhileReady()
{
AddStep("setup", () =>
{
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
});
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0));
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
AddAssert("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
}
[TestCase(true)]
[TestCase(false)]
public void TestManyUsersChangingState(bool isHost)
{
const int users = 10;
AddStep("setup", () =>
{
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
for (int i = 0; i < users; i++)
MultiplayerClient.AddUser(new APIUser { Id = i, Username = "Another user" });
});
if (!isHost)
AddStep("transfer host", () => MultiplayerClient.TransferHost(2));
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddRepeatStep("change user ready state", () =>
{
MultiplayerClient.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle);
}, 20);
AddRepeatStep("ready all users", () =>
{
var nextUnready = MultiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
if (nextUnready != null)
MultiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
}, users);
}
private void verifyGameplayStartFlow()
{
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
AddAssert("ready button disabled", () => !button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
AddStep("transitioned to gameplay", () => readyClickOperation.Dispose());
AddStep("finish gameplay", () =>
{
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded);
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay);
});
AddUntilStep("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
}
}
}

View File

@ -1,9 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
@ -11,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -28,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneMultiplayerSpectateButton : MultiplayerTestScene public class TestSceneMultiplayerSpectateButton : MultiplayerTestScene
{ {
private MultiplayerSpectateButton spectateButton; private MultiplayerSpectateButton spectateButton;
private MultiplayerReadyButton readyButton; private MatchStartControl startControl;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>(); private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
@ -36,8 +35,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
private BeatmapManager beatmaps; private BeatmapManager beatmaps;
private RulesetStore rulesets; private RulesetStore rulesets;
private IDisposable readyClickOperation;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio) private void load(GameHost host, AudioManager audio)
{ {
@ -60,49 +57,26 @@ namespace osu.Game.Tests.Visual.Multiplayer
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID,
}; };
Child = new FillFlowContainer Child = new PopoverContainer
{ {
AutoSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical, Child = new FillFlowContainer
Children = new Drawable[]
{ {
spectateButton = new MultiplayerSpectateButton AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{ {
Anchor = Anchor.Centre, spectateButton = new MultiplayerSpectateButton
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
OnSpectateClick = () =>
{ {
readyClickOperation = OngoingOperationTracker.BeginOperation(); Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Task.Run(async () => Size = new Vector2(200, 50),
{ },
await MultiplayerClient.ToggleSpectate(); startControl = new MatchStartControl
readyClickOperation.Dispose();
});
}
},
readyButton = new MultiplayerReadyButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
OnReadyClick = () =>
{ {
readyClickOperation = OngoingOperationTracker.BeginOperation(); Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Task.Run(async () => Size = new Vector2(200, 50),
{
if (MultiplayerClient.IsHost && MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready)
{
await MultiplayerClient.StartMatch();
return;
}
await MultiplayerClient.ToggleReady();
readyClickOperation.Dispose();
});
} }
} }
} }
@ -172,6 +146,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddUntilStep($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled); => AddUntilStep($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);
private void assertReadyButtonEnablement(bool shouldBeEnabled) private void assertReadyButtonEnablement(bool shouldBeEnabled)
=> AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => readyButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled); => AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => startControl.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value == shouldBeEnabled);
} }
} }

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -14,6 +15,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Toolbar; using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
@ -21,10 +23,12 @@ using osu.Game.Scoring;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Screens.Select.Options; using osu.Game.Screens.Select.Options;
using osu.Game.Skinning.Editor;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -66,6 +70,73 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
} }
[Test]
public void TestEditComponentDuringGameplay()
{
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
SkinEditor skinEditor = null;
AddStep("open skin editor", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.S);
InputManager.ReleaseKey(Key.ControlLeft);
InputManager.ReleaseKey(Key.ShiftLeft);
});
AddUntilStep("get skin editor", () => (skinEditor = Game.ChildrenOfType<SkinEditor>().FirstOrDefault()) != null);
AddStep("Click gameplay scene button", () =>
{
skinEditor.ChildrenOfType<SkinEditorSceneLibrary.SceneButton>().First(b => b.Text == "Gameplay").TriggerClick();
});
AddUntilStep("wait for player", () =>
{
// dismiss any notifications that may appear (ie. muted notification).
clickMouseInCentre();
return Game.ScreenStack.CurrentScreen is Player;
});
BarHitErrorMeter hitErrorMeter = null;
AddUntilStep("select bar hit error blueprint", () =>
{
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().FirstOrDefault(b => b.Item is BarHitErrorMeter);
if (blueprint == null)
return false;
hitErrorMeter = (BarHitErrorMeter)blueprint.Item;
skinEditor.SelectedComponents.Clear();
skinEditor.SelectedComponents.Add(blueprint.Item);
return true;
});
AddAssert("value is default", () => hitErrorMeter.JudgementLineThickness.IsDefault);
AddStep("hover first slider", () =>
{
InputManager.MoveMouseTo(
skinEditor.ChildrenOfType<SkinSettingsToolbox>().First()
.ChildrenOfType<SettingsSlider<float>>().First()
.ChildrenOfType<SliderBar<float>>().First()
);
});
AddStep("adjust slider via keyboard", () => InputManager.Key(Key.Left));
AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default);
}
[Test] [Test]
public void TestRetryCountIncrements() public void TestRetryCountIncrements()
{ {
@ -120,7 +191,8 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action()); AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
AddStep("show local scores", () => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local)); AddStep("show local scores",
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<LeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null); AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<LeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null);
@ -133,7 +205,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); AddStep("confirm deletion", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog == null); AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog == null);
AddAssert("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true)); AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true));
AddUntilStep("wait for score panel removal", () => scorePanel.Parent == null); AddUntilStep("wait for score panel removal", () => scorePanel.Parent == null);
} }
@ -152,7 +224,8 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action()); AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
AddStep("show local scores", () => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local)); AddStep("show local scores",
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<LeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null); AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<LeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null);
@ -178,7 +251,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); AddStep("confirm deletion", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog == null); AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog == null);
AddAssert("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true)); AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true));
AddUntilStep("wait for score panel removal", () => scorePanel.Parent == null); AddUntilStep("wait for score panel removal", () => scorePanel.Parent == null);
} }
@ -262,6 +335,20 @@ namespace osu.Game.Tests.Visual.Navigation
exitViaBackButtonAndConfirm(); exitViaBackButtonAndConfirm();
} }
[Test]
public void TestModsResetOnEnteringMultiplayer()
{
var osuAutomationMod = new OsuModAutoplay();
AddStep("Enable autoplay", () => { Game.SelectedMods.Value = new[] { osuAutomationMod }; });
PushAndConfirm(() => new Screens.OnlinePlay.Multiplayer.Multiplayer());
AddUntilStep("Mods are removed", () => Game.SelectedMods.Value.Count == 0);
AddStep("Return to menu", () => Game.ScreenStack.CurrentScreen.Exit());
AddUntilStep("Mods are restored", () => Game.SelectedMods.Value.Contains(osuAutomationMod));
}
[Test] [Test]
public void TestExitMultiWithEscape() public void TestExitMultiWithEscape()
{ {

View File

@ -0,0 +1,163 @@
// 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 System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.Chat.ChannelList;
using osuTK;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public class TestSceneChannelListItem : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
[Cached]
private readonly Bindable<Channel> selected = new Bindable<Channel>();
private static readonly List<Channel> channels = new List<Channel>
{
createPublicChannel("#public-channel"),
createPublicChannel("#public-channel-long-name"),
createPrivateChannel("test user", 2),
createPrivateChannel("test user long name", 3),
};
private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>();
private FillFlowContainer flow;
private OsuSpriteText selectedText;
private OsuSpriteText leaveText;
[SetUp]
public void SetUp()
{
Schedule(() =>
{
foreach (var item in channelMap.Values)
item.Expire();
channelMap.Clear();
Child = new FillFlowContainer
{
Direction = FillDirection.Vertical,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(10),
Children = new Drawable[]
{
selectedText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
leaveText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Height = 16,
AlwaysPresent = true,
},
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Y,
Width = 190,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background6,
},
flow = new FillFlowContainer
{
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
},
},
},
};
selected.BindValueChanged(change =>
{
selectedText.Text = $"Selected Channel: {change.NewValue?.Name ?? "[null]"}";
}, true);
foreach (var channel in channels)
{
var item = new ChannelListItem(channel);
flow.Add(item);
channelMap.Add(channel, item);
item.OnRequestSelect += c => selected.Value = c;
item.OnRequestLeave += leaveChannel;
}
});
}
[Test]
public void TestVisual()
{
AddStep("Select second item", () => selected.Value = channels.Skip(1).First());
AddStep("Unread Selected", () =>
{
if (selected.Value != null)
channelMap[selected.Value].Unread.Value = true;
});
AddStep("Read Selected", () =>
{
if (selected.Value != null)
channelMap[selected.Value].Unread.Value = false;
});
AddStep("Add Mention Selected", () =>
{
if (selected.Value != null)
channelMap[selected.Value].Mentions.Value++;
});
AddStep("Add 98 Mentions Selected", () =>
{
if (selected.Value != null)
channelMap[selected.Value].Mentions.Value += 98;
});
AddStep("Clear Mentions Selected", () =>
{
if (selected.Value != null)
channelMap[selected.Value].Mentions.Value = 0;
});
}
private void leaveChannel(Channel channel)
{
leaveText.Text = $"OnRequestLeave: {channel.Name}";
leaveText.FadeOutFromOne(1000, Easing.InQuint);
}
private static Channel createPublicChannel(string name) =>
new Channel { Name = name, Type = ChannelType.Public, Id = 1234 };
private static Channel createPrivateChannel(string username, int id)
=> new Channel(new APIUser { Id = id, Username = username });
}
}

View File

@ -0,0 +1,90 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.Chat.Listing;
using osuTK;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public class TestSceneChannelListing : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider overlayColours = new OverlayColourProvider(OverlayColourScheme.Pink);
private SearchTextBox search;
private ChannelListing listing;
[SetUp]
public void SetUp()
{
Schedule(() =>
{
Children = new Drawable[]
{
search = new SearchTextBox
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 300,
Margin = new MarginPadding { Top = 100 },
},
listing = new ChannelListing
{
Size = new Vector2(800, 400),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
};
listing.Show();
search.Current.ValueChanged += term => listing.SearchTerm = term.NewValue;
});
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Add Join/Leave callbacks", () =>
{
listing.OnRequestJoin += channel => channel.Joined.Value = true;
listing.OnRequestLeave += channel => channel.Joined.Value = false;
});
}
[Test]
public void TestAddRandomChannels()
{
AddStep("Add Random Channels", () =>
{
listing.UpdateAvailableChannels(createRandomChannels(20));
});
}
private Channel createRandomChannel()
{
int id = RNG.Next(0, 10000);
return new Channel
{
Name = $"#channel-{id}",
Topic = RNG.Next(4) < 3 ? $"We talk about the number {id} here" : null,
Type = ChannelType.Public,
Id = id,
};
}
private List<Channel> createRandomChannels(int num)
=> Enumerable.Range(0, num)
.Select(_ => createRandomChannel())
.ToList();
}
}

View File

@ -122,6 +122,8 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestHideOverlay() public void TestHideOverlay()
{ {
AddStep("Open chat overlay", () => chatOverlay.Show());
AddAssert("Chat overlay is visible", () => chatOverlay.State.Value == Visibility.Visible); AddAssert("Chat overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
@ -134,6 +136,7 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestChannelSelection() public void TestChannelSelection()
{ {
AddStep("Open chat overlay", () => chatOverlay.Show());
AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
AddStep("Setup get message response", () => onGetMessages = channel => AddStep("Setup get message response", () => onGetMessages = channel =>
{ {
@ -169,6 +172,7 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestSearchInSelector() public void TestSearchInSelector()
{ {
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Search for 'no. 2'", () => chatOverlay.ChildrenOfType<SearchTextBox>().First().Text = "no. 2"); AddStep("Search for 'no. 2'", () => chatOverlay.ChildrenOfType<SearchTextBox>().First().Text = "no. 2");
AddUntilStep("Only channel 2 visible", () => AddUntilStep("Only channel 2 visible", () =>
{ {
@ -180,6 +184,7 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestChannelShortcutKeys() public void TestChannelShortcutKeys()
{ {
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join channels", () => channels.ForEach(channel => channelManager.JoinChannel(channel))); AddStep("Join channels", () => channels.ForEach(channel => channelManager.JoinChannel(channel)));
AddStep("Close channel selector", () => InputManager.Key(Key.Escape)); AddStep("Close channel selector", () => InputManager.Key(Key.Escape));
AddUntilStep("Wait for close", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); AddUntilStep("Wait for close", () => chatOverlay.SelectionOverlayState == Visibility.Hidden);
@ -199,6 +204,7 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestCloseChannelBehaviour() public void TestCloseChannelBehaviour()
{ {
AddStep("Open chat overlay", () => chatOverlay.Show());
AddUntilStep("Join until dropdown has channels", () => AddUntilStep("Join until dropdown has channels", () =>
{ {
if (visibleChannels.Count() < joinedChannels.Count()) if (visibleChannels.Count() < joinedChannels.Count())
@ -269,6 +275,7 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestChannelCloseButton() public void TestChannelCloseButton()
{ {
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join 2 channels", () => AddStep("Join 2 channels", () =>
{ {
channelManager.JoinChannel(channel1); channelManager.JoinChannel(channel1);
@ -289,6 +296,7 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestCloseTabShortcut() public void TestCloseTabShortcut()
{ {
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join 2 channels", () => AddStep("Join 2 channels", () =>
{ {
channelManager.JoinChannel(channel1); channelManager.JoinChannel(channel1);
@ -314,6 +322,7 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestNewTabShortcut() public void TestNewTabShortcut()
{ {
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join 2 channels", () => AddStep("Join 2 channels", () =>
{ {
channelManager.JoinChannel(channel1); channelManager.JoinChannel(channel1);
@ -330,6 +339,7 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestRestoreTabShortcut() public void TestRestoreTabShortcut()
{ {
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join 3 channels", () => AddStep("Join 3 channels", () =>
{ {
channelManager.JoinChannel(channel1); channelManager.JoinChannel(channel1);
@ -375,6 +385,7 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestChatCommand() public void TestChatCommand()
{ {
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
@ -398,6 +409,8 @@ namespace osu.Game.Tests.Visual.Online
{ {
Channel multiplayerChannel = null; Channel multiplayerChannel = null;
AddStep("open chat overlay", () => chatOverlay.Show());
AddStep("join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser()) AddStep("join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser())
{ {
Name = "#mp_1", Name = "#mp_1",
@ -412,6 +425,122 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("channel left", () => !channelManager.JoinedChannels.Contains(multiplayerChannel)); AddAssert("channel left", () => !channelManager.JoinedChannels.Contains(multiplayerChannel));
} }
[Test]
public void TestHighlightOnCurrentChannel()
{
Message message = null;
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
AddStep("Send message in channel 1", () =>
{
channel1.AddNewMessages(message = new Message
{
ChannelId = channel1.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = new APIUser
{
Id = 2,
Username = "Someone",
}
});
});
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel1));
}
[Test]
public void TestHighlightOnAnotherChannel()
{
Message message = null;
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
AddStep("Join channel 2", () => channelManager.JoinChannel(channel2));
AddStep("Send message in channel 2", () =>
{
channel2.AddNewMessages(message = new Message
{
ChannelId = channel2.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = new APIUser
{
Id = 2,
Username = "Someone",
}
});
});
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel2));
AddAssert("Switched to channel 2", () => channelManager.CurrentChannel.Value == channel2);
}
[Test]
public void TestHighlightOnLeftChannel()
{
Message message = null;
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
AddStep("Join channel 2", () => channelManager.JoinChannel(channel2));
AddStep("Send message in channel 2", () =>
{
channel2.AddNewMessages(message = new Message
{
ChannelId = channel2.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = new APIUser
{
Id = 2,
Username = "Someone",
}
});
});
AddStep("Leave channel 2", () => channelManager.LeaveChannel(channel2));
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel2));
AddAssert("Switched to channel 2", () => channelManager.CurrentChannel.Value == channel2);
}
[Test]
public void TestHighlightWhileChatNeverOpen()
{
Message message = null;
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
AddStep("Send message in channel 1", () =>
{
channel1.AddNewMessages(message = new Message
{
ChannelId = channel1.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = new APIUser
{
Id = 2,
Username = "Someone",
}
});
});
AddStep("Highlight message and open chat", () =>
{
chatOverlay.HighlightMessage(message, channel1);
chatOverlay.Show();
});
}
private void pressChannelHotkey(int number) private void pressChannelHotkey(int number)
{ {
var channelKey = Key.Number0 + number; var channelKey = Key.Number0 + number;
@ -456,8 +585,6 @@ namespace osu.Game.Tests.Visual.Online
ChannelManager, ChannelManager,
ChatOverlay = new TestChatOverlay { RelativeSizeAxes = Axes.Both, }, ChatOverlay = new TestChatOverlay { RelativeSizeAxes = Axes.Both, },
}; };
ChatOverlay.Show();
} }
} }

View File

@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Online
private int messageIdCounter; private int messageIdCounter;
[SetUp] [SetUp]
public void Setup() public void Setup() => Schedule(() =>
{ {
if (API is DummyAPIAccess daa) if (API is DummyAPIAccess daa)
{ {
@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Online
testContainer.ChatOverlay.Show(); testContainer.ChatOverlay.Show();
}); });
} });
private bool dummyAPIHandleRequest(APIRequest request) private bool dummyAPIHandleRequest(APIRequest request)
{ {

View File

@ -9,6 +9,8 @@ using System;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Chat; using osu.Game.Overlays.Chat;
using osuTK.Input; using osuTK.Input;
@ -107,49 +109,7 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestManyMessages() public void TestManyMessages()
{ {
AddStep("message from admin", () => testChannel.AddNewMessages(new Message(messageIdSequence++) sendRegularMessages();
{
Sender = admin,
Content = "I am a wang!"
}));
AddStep("message from team red", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = redUser,
Content = "I am team red."
}));
AddStep("message from team red", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = redUser,
Content = "I plan to win!"
}));
AddStep("message from team blue", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = blueUser,
Content = "Not on my watch. Prepare to eat saaaaaaaaaand. Lots and lots of saaaaaaand."
}));
AddStep("message from admin", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = admin,
Content = "Okay okay, calm down guys. Let's do this!"
}));
AddStep("message from long username", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = longUsernameUser,
Content = "Hi guys, my new username is lit!"
}));
AddStep("message with new date", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = longUsernameUser,
Content = "Message from the future!",
Timestamp = DateTimeOffset.Now
}));
checkScrolledToBottom(); checkScrolledToBottom();
const int messages_per_call = 10; const int messages_per_call = 10;
@ -182,6 +142,64 @@ namespace osu.Game.Tests.Visual.Online
checkScrolledToBottom(); checkScrolledToBottom();
} }
[Test]
public void TestMessageHighlighting()
{
Message highlighted = null;
sendRegularMessages();
AddStep("highlight first message", () =>
{
highlighted = testChannel.Messages[0];
testChannel.HighlightedMessage.Value = highlighted;
});
AddUntilStep("chat scrolled to first message", () =>
{
var line = chatDisplay.ChildrenOfType<ChatLine>().Single(c => c.Message == highlighted);
return chatDisplay.ScrollContainer.ScreenSpaceDrawQuad.Contains(line.ScreenSpaceDrawQuad.Centre);
});
sendMessage();
checkNotScrolledToBottom();
AddStep("highlight last message", () =>
{
highlighted = testChannel.Messages[^1];
testChannel.HighlightedMessage.Value = highlighted;
});
AddUntilStep("chat scrolled to last message", () =>
{
var line = chatDisplay.ChildrenOfType<ChatLine>().Single(c => c.Message == highlighted);
return chatDisplay.ScrollContainer.ScreenSpaceDrawQuad.Contains(line.ScreenSpaceDrawQuad.Centre);
});
sendMessage();
checkScrolledToBottom();
AddRepeatStep("highlight other random messages", () =>
{
highlighted = testChannel.Messages[RNG.Next(0, testChannel.Messages.Count - 1)];
testChannel.HighlightedMessage.Value = highlighted;
}, 10);
}
[Test]
public void TestMessageHighlightingOnFilledChat()
{
int index = 0;
fillChat(100);
AddStep("highlight first message", () => testChannel.HighlightedMessage.Value = testChannel.Messages[index = 0]);
AddStep("highlight next message", () => testChannel.HighlightedMessage.Value = testChannel.Messages[index = Math.Min(index + 1, testChannel.Messages.Count - 1)]);
AddStep("highlight last message", () => testChannel.HighlightedMessage.Value = testChannel.Messages[index = testChannel.Messages.Count - 1]);
AddStep("highlight previous message", () => testChannel.HighlightedMessage.Value = testChannel.Messages[index = Math.Max(index - 1, 0)]);
AddRepeatStep("highlight random messages", () => testChannel.HighlightedMessage.Value = testChannel.Messages[index = RNG.Next(0, testChannel.Messages.Count - 1)], 10);
}
/// <summary> /// <summary>
/// Tests that when a message gets wrapped by the chat display getting contracted while scrolled to bottom, the chat will still keep scrolling down. /// Tests that when a message gets wrapped by the chat display getting contracted while scrolled to bottom, the chat will still keep scrolling down.
/// </summary> /// </summary>
@ -286,11 +304,11 @@ namespace osu.Game.Tests.Visual.Online
checkScrolledToBottom(); checkScrolledToBottom();
} }
private void fillChat() private void fillChat(int count = 10)
{ {
AddStep("fill chat", () => AddStep("fill chat", () =>
{ {
for (int i = 0; i < 10; i++) for (int i = 0; i < count; i++)
{ {
testChannel.AddNewMessages(new Message(messageIdSequence++) testChannel.AddNewMessages(new Message(messageIdSequence++)
{ {
@ -321,6 +339,52 @@ namespace osu.Game.Tests.Visual.Online
})); }));
} }
private void sendRegularMessages()
{
AddStep("message from admin", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = admin,
Content = "I am a wang!"
}));
AddStep("message from team red", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = redUser,
Content = "I am team red."
}));
AddStep("message from team red", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = redUser,
Content = "I plan to win!"
}));
AddStep("message from team blue", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = blueUser,
Content = "Not on my watch. Prepare to eat saaaaaaaaaand. Lots and lots of saaaaaaand."
}));
AddStep("message from admin", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = admin,
Content = "Okay okay, calm down guys. Let's do this!"
}));
AddStep("message from long username", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = longUsernameUser,
Content = "Hi guys, my new username is lit!"
}));
AddStep("message with new date", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = longUsernameUser,
Content = "Message from the future!",
Timestamp = DateTimeOffset.Now
}));
}
private void checkScrolledToBottom() => private void checkScrolledToBottom() =>
AddUntilStep("is scrolled to bottom", () => chatDisplay.ScrolledToBottom); AddUntilStep("is scrolled to bottom", () => chatDisplay.ScrolledToBottom);

View File

@ -40,24 +40,33 @@ namespace osu.Game.Tests.Visual.Playlists
private int totalCount; private int totalCount;
private ScoreInfo userScore; private ScoreInfo userScore;
[SetUp] [SetUpSteps]
public void Setup() => Schedule(() => public override void SetUpSteps()
{ {
lowestScoreId = 1; base.SetUpSteps();
highestScoreId = 1;
requestComplete = false;
totalCount = 0;
userScore = TestResources.CreateTestScoreInfo(); // Previous test instances of the results screen may still exist at this point so wait for
userScore.TotalScore = 0; // those screens to be cleaned up by the base SetUpSteps before re-initialising test state.
userScore.Statistics = new Dictionary<HitResult, int>(); // The the screen also holds a leased Beatmap bindable so reassigning it must happen after
// the screen as been exited.
AddStep("initialise user scores and beatmap", () =>
{
lowestScoreId = 1;
highestScoreId = 1;
requestComplete = false;
totalCount = 0;
bindHandler(); userScore = TestResources.CreateTestScoreInfo();
userScore.TotalScore = 0;
userScore.Statistics = new Dictionary<HitResult, int>();
// beatmap is required to be an actual beatmap so the scores can get their scores correctly calculated for standardised scoring. bindHandler();
// else the tests that rely on ordering will fall over.
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); // Beatmap is required to be an actual beatmap so the scores can get their scores correctly
}); // calculated for standardised scoring, else the tests that rely on ordering will fall over.
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
});
}
[Test] [Test]
public void TestShowWithUserScore() public void TestShowWithUserScore()

View File

@ -390,7 +390,7 @@ namespace osu.Game.Tests.Visual.Ranking
private class RulesetWithNoPerformanceCalculator : OsuRuleset private class RulesetWithNoPerformanceCalculator : OsuRuleset
{ {
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => null; public override PerformanceCalculator CreatePerformanceCalculator() => null;
} }
} }
} }

View File

@ -0,0 +1,97 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays;
using osu.Game.Screens.Edit.Components;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneEditorSidebar : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[Test]
public void TestSidebars()
{
AddStep("Add sidebars", () =>
{
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
new EditorSidebar
{
Children = new[]
{
new EditorSidebarSection("Section 1")
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
Spacing = new Vector2(3),
ChildrenEnumerable = Enumerable.Range(0, 10).Select(_ => new Box
{
Colour = Color4.White,
Size = new Vector2(32),
})
},
},
new EditorSidebarSection("Section with a really long section header")
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
Spacing = new Vector2(3),
ChildrenEnumerable = Enumerable.Range(0, 400).Select(_ => new Box
{
Colour = Color4.Gray,
Size = new Vector2(32),
})
},
},
},
},
new Container
{
RelativeSizeAxes = Axes.Both,
},
new EditorSidebar
{
Children = new[]
{
new EditorSidebarSection("Section 1"),
new EditorSidebarSection("Section 2"),
},
},
}
}
}
};
});
}
}
}

View File

@ -40,6 +40,10 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
Text = @"You're a fake!", Text = @"You're a fake!",
}, },
new PopupDialogDangerousButton
{
Text = @"Careful with this one..",
},
}; };
} }
} }

View File

@ -8,7 +8,7 @@
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="Moq" Version="4.17.2" />
</ItemGroup> </ItemGroup>
<PropertyGroup Label="Project"> <PropertyGroup Label="Project">
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>

View File

@ -26,7 +26,7 @@ namespace osu.Game.Tournament.Screens.Setup
dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true); dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true);
Action = () => game.GracefullyExit(); Action = () => game.GracefullyExit();
folderButton.Action = storage.PresentExternally; folderButton.Action = () => storage.PresentExternally();
ButtonText = "Close osu!"; ButtonText = "Close osu!";
} }

View File

@ -9,6 +9,7 @@ using osu.Framework.Testing;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Scoring; using osu.Game.Scoring;
using Realms; using Realms;
@ -169,7 +170,12 @@ namespace osu.Game.Beatmaps
[Ignored] [Ignored]
public APIBeatmap? OnlineInfo { get; set; } public APIBeatmap? OnlineInfo { get; set; }
/// <summary>
/// The maximum achievable combo on this beatmap, populated for online info purposes only.
/// Todo: This should never be used nor exist, but is still relied on in <see cref="ScoresContainer.Scores"/> since <see cref="IBeatmapInfo"/> can't be used yet. For now this is obsoleted until it is removed.
/// </summary>
[Ignored] [Ignored]
[Obsolete("Use ScoreManager.GetMaximumAchievableComboAsync instead.")]
public int? MaxCombo { get; set; } public int? MaxCombo { get; set; }
[Ignored] [Ignored]

View File

@ -53,9 +53,6 @@ namespace osu.Game.Beatmaps
[NotMapped] [NotMapped]
public APIBeatmap OnlineInfo { get; set; } public APIBeatmap OnlineInfo { get; set; }
[NotMapped]
public int? MaxCombo { get; set; }
/// <summary> /// <summary>
/// The playable length in milliseconds of this beatmap. /// The playable length in milliseconds of this beatmap.
/// </summary> /// </summary>

View File

@ -19,6 +19,11 @@ namespace osu.Game.Beatmaps.Formats
{ {
public class LegacyBeatmapDecoder : LegacyDecoder<Beatmap> public class LegacyBeatmapDecoder : LegacyDecoder<Beatmap>
{ {
/// <summary>
/// An offset which needs to be applied to old beatmaps (v4 and lower) to correct timing changes that were applied at a game client level.
/// </summary>
public const int EARLY_VERSION_TIMING_OFFSET = 24;
internal static RulesetStore RulesetStore; internal static RulesetStore RulesetStore;
private Beatmap beatmap; private Beatmap beatmap;
@ -50,8 +55,7 @@ namespace osu.Game.Beatmaps.Formats
RulesetStore = new AssemblyRulesetStore(); RulesetStore = new AssemblyRulesetStore();
} }
// BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) offset = FormatVersion < 5 ? EARLY_VERSION_TIMING_OFFSET : 0;
offset = FormatVersion < 5 ? 24 : 0;
} }
protected override Beatmap CreateTemplateObject() protected override Beatmap CreateTemplateObject()

View File

@ -6,6 +6,7 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -170,6 +171,39 @@ namespace osu.Game.Configuration
private static readonly ConcurrentDictionary<Type, (SettingSourceAttribute, PropertyInfo)[]> property_info_cache = new ConcurrentDictionary<Type, (SettingSourceAttribute, PropertyInfo)[]>(); private static readonly ConcurrentDictionary<Type, (SettingSourceAttribute, PropertyInfo)[]> property_info_cache = new ConcurrentDictionary<Type, (SettingSourceAttribute, PropertyInfo)[]>();
/// <summary>
/// Returns the underlying value of the given mod setting object.
/// Can be used for serialization and equality comparison purposes.
/// </summary>
/// <param name="setting">A <see cref="SettingSourceAttribute"/> bindable.</param>
public static object GetUnderlyingSettingValue(this object setting)
{
switch (setting)
{
case Bindable<double> d:
return d.Value;
case Bindable<int> i:
return i.Value;
case Bindable<float> f:
return f.Value;
case Bindable<bool> b:
return b.Value;
case IBindable u:
// An unknown (e.g. enum) generic type.
var valueMethod = u.GetType().GetProperty(nameof(IBindable<int>.Value));
Debug.Assert(valueMethod != null);
return valueMethod.GetValue(u);
default:
// fall back for non-bindable cases.
return setting;
}
}
public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetSettingsSourceProperties(this object obj) public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetSettingsSourceProperties(this object obj)
{ {
var type = obj.GetType(); var type = obj.GetType();

View File

@ -295,7 +295,6 @@ namespace osu.Game.Database
TimelineZoom = beatmap.TimelineZoom, TimelineZoom = beatmap.TimelineZoom,
Countdown = beatmap.Countdown, Countdown = beatmap.Countdown,
CountdownOffset = beatmap.CountdownOffset, CountdownOffset = beatmap.CountdownOffset,
MaxCombo = beatmap.MaxCombo,
Bookmarks = beatmap.Bookmarks, Bookmarks = beatmap.Bookmarks,
BeatmapSet = realmBeatmapSet, BeatmapSet = realmBeatmapSet,
}; };

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
@ -17,6 +19,7 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Statistics; using osu.Framework.Statistics;
using osu.Framework.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
@ -28,8 +31,6 @@ using osu.Game.Stores;
using Realms; using Realms;
using Realms.Exceptions; using Realms.Exceptions;
#nullable enable
namespace osu.Game.Database namespace osu.Game.Database
{ {
/// <summary> /// <summary>
@ -46,6 +47,8 @@ namespace osu.Game.Database
private readonly IDatabaseContextFactory? efContextFactory; private readonly IDatabaseContextFactory? efContextFactory;
private readonly SynchronizationContext? updateThreadSyncContext;
/// <summary> /// <summary>
/// Version history: /// Version history:
/// 6 ~2021-10-18 First tracked version. /// 6 ~2021-10-18 First tracked version.
@ -143,12 +146,15 @@ namespace osu.Game.Database
/// </summary> /// </summary>
/// <param name="storage">The game storage which will be used to create the realm backing file.</param> /// <param name="storage">The game storage which will be used to create the realm backing file.</param>
/// <param name="filename">The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified.</param> /// <param name="filename">The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified.</param>
/// <param name="updateThread">The game update thread, used to post realm operations into a thread-safe context.</param>
/// <param name="efContextFactory">An EF factory used only for migration purposes.</param> /// <param name="efContextFactory">An EF factory used only for migration purposes.</param>
public RealmAccess(Storage storage, string filename, IDatabaseContextFactory? efContextFactory = null) public RealmAccess(Storage storage, string filename, GameThread? updateThread = null, IDatabaseContextFactory? efContextFactory = null)
{ {
this.storage = storage; this.storage = storage;
this.efContextFactory = efContextFactory; this.efContextFactory = efContextFactory;
updateThreadSyncContext = updateThread?.SynchronizationContext ?? SynchronizationContext.Current;
Filename = filename; Filename = filename;
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
@ -379,9 +385,6 @@ namespace osu.Game.Database
public IDisposable RegisterForNotifications<T>(Func<Realm, IQueryable<T>> query, NotificationCallbackDelegate<T> callback) public IDisposable RegisterForNotifications<T>(Func<Realm, IQueryable<T>> query, NotificationCallbackDelegate<T> callback)
where T : RealmObjectBase where T : RealmObjectBase
{ {
if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread.");
lock (realmLock) lock (realmLock)
{ {
Func<Realm, IDisposable?> action = realm => query(realm).QueryAsyncWithNotifications(callback); Func<Realm, IDisposable?> action = realm => query(realm).QueryAsyncWithNotifications(callback);
@ -459,23 +462,24 @@ namespace osu.Game.Database
/// <returns>An <see cref="IDisposable"/> which should be disposed to unsubscribe any inner subscription.</returns> /// <returns>An <see cref="IDisposable"/> which should be disposed to unsubscribe any inner subscription.</returns>
public IDisposable RegisterCustomSubscription(Func<Realm, IDisposable?> action) public IDisposable RegisterCustomSubscription(Func<Realm, IDisposable?> action)
{ {
if (!ThreadSafety.IsUpdateThread) if (updateThreadSyncContext == null)
throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread."); throw new InvalidOperationException("Attempted to register a realm subscription before update thread registration.");
var syncContext = SynchronizationContext.Current;
total_subscriptions.Value++; total_subscriptions.Value++;
registerSubscription(action); if (ThreadSafety.IsUpdateThread)
updateThreadSyncContext.Send(_ => registerSubscription(action), null);
else
updateThreadSyncContext.Post(_ => registerSubscription(action), null);
// This token is returned to the consumer. // This token is returned to the consumer.
// When disposed, it will cause the registration to be permanently ceased (unsubscribed with realm and unregistered by this class). // When disposed, it will cause the registration to be permanently ceased (unsubscribed with realm and unregistered by this class).
return new InvokeOnDisposal(() => return new InvokeOnDisposal(() =>
{ {
if (ThreadSafety.IsUpdateThread) if (ThreadSafety.IsUpdateThread)
syncContext.Send(_ => unsubscribe(), null); updateThreadSyncContext.Send(_ => unsubscribe(), null);
else else
syncContext.Post(_ => unsubscribe(), null); updateThreadSyncContext.Post(_ => unsubscribe(), null);
void unsubscribe() void unsubscribe()
{ {

View File

@ -1,8 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using Humanizer;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -59,8 +62,18 @@ namespace osu.Game.Extensions
component.Origin = info.Origin; component.Origin = info.Origin;
if (component is ISkinnableDrawable skinnable) if (component is ISkinnableDrawable skinnable)
{
skinnable.UsesFixedAnchor = info.UsesFixedAnchor; skinnable.UsesFixedAnchor = info.UsesFixedAnchor;
foreach (var (_, property) in component.GetSettingsSourceProperties())
{
if (!info.Settings.TryGetValue(property.Name.Underscore(), out object settingValue))
continue;
skinnable.CopyAdjustedSetting((IBindable)property.GetValue(component), settingValue);
}
}
if (component is Container container) if (component is Container container)
{ {
foreach (var child in info.Children) foreach (var child in info.Children)

View File

@ -28,6 +28,14 @@ namespace osu.Game.Graphics.Containers
/// </summary> /// </summary>
protected virtual bool AllowMultipleFires => false; protected virtual bool AllowMultipleFires => false;
/// <summary>
/// Specify a custom activation delay, overriding the game-wide user setting.
/// </summary>
/// <remarks>
/// This should be used in special cases where we want to be extra sure the user knows what they are doing. An example is when changes would be lost.
/// </remarks>
protected virtual double? HoldActivationDelay => null;
public Bindable<double> Progress = new BindableDouble(); public Bindable<double> Progress = new BindableDouble();
private Bindable<double> holdActivationDelay; private Bindable<double> holdActivationDelay;
@ -35,7 +43,9 @@ namespace osu.Game.Graphics.Containers
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
holdActivationDelay = config.GetBindable<double>(OsuSetting.UIHoldActivationDelay); holdActivationDelay = HoldActivationDelay != null
? new Bindable<double>(HoldActivationDelay.Value)
: config.GetBindable<double>(OsuSetting.UIHoldActivationDelay);
} }
protected void BeginConfirm() protected void BeginConfirm()

View File

@ -6,6 +6,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Layout;
using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Screens; using osu.Game.Screens;
@ -66,7 +68,7 @@ namespace osu.Game.Graphics.Containers
this.targetMode = targetMode; this.targetMode = targetMode;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
InternalChild = sizableContainer = new AlwaysInputContainer InternalChild = sizableContainer = new SizeableAlwaysInputContainer(targetMode == ScalingMode.Everything)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both, RelativePositionAxes = Axes.Both,
@ -209,13 +211,50 @@ namespace osu.Game.Graphics.Containers
} }
} }
private class AlwaysInputContainer : Container private class SizeableAlwaysInputContainer : Container
{ {
[Resolved]
private GameHost host { get; set; }
[Resolved]
private ISafeArea safeArea { get; set; }
private readonly bool confineHostCursor;
private readonly LayoutValue cursorRectCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public AlwaysInputContainer() /// <summary>
/// Container used for sizing/positioning purposes in <see cref="ScalingContainer"/>. Always receives mouse input.
/// </summary>
/// <param name="confineHostCursor">Whether to confine the host cursor to the draw area of this container.</param>
/// <remarks>Cursor confinement will abide by the <see cref="OsuSetting.ConfineMouseMode"/> setting.</remarks>
public SizeableAlwaysInputContainer(bool confineHostCursor)
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
this.confineHostCursor = confineHostCursor;
if (confineHostCursor)
AddLayout(cursorRectCache);
}
protected override void Update()
{
base.Update();
if (confineHostCursor && !cursorRectCache.IsValid)
{
updateHostCursorConfineRect();
cursorRectCache.Validate();
}
}
private void updateHostCursorConfineRect()
{
if (host.Window == null) return;
bool coversWholeScreen = Size == Vector2.One && safeArea.SafeAreaPadding.Value.Total == Vector2.Zero;
host.Window.CursorConfineRect = coversWholeScreen ? (RectangleF?)null : ToScreenSpace(DrawRectangle).AABBFloat;
} }
} }
} }

View File

@ -45,8 +45,9 @@ namespace osu.Game.Graphics.UserInterface
} }
} }
protected readonly Container ColourContainer;
private readonly Container backgroundContainer; private readonly Container backgroundContainer;
private readonly Container colourContainer;
private readonly Container glowContainer; private readonly Container glowContainer;
private readonly Box leftGlow; private readonly Box leftGlow;
private readonly Box centerGlow; private readonly Box centerGlow;
@ -113,7 +114,7 @@ namespace osu.Game.Graphics.UserInterface
Masking = true, Masking = true,
Children = new Drawable[] Children = new Drawable[]
{ {
colourContainer = new Container ColourContainer = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -182,7 +183,7 @@ namespace osu.Game.Graphics.UserInterface
{ {
buttonColour = value; buttonColour = value;
updateGlow(); updateGlow();
colourContainer.Colour = value; ColourContainer.Colour = value;
} }
} }
@ -230,11 +231,11 @@ namespace osu.Game.Graphics.UserInterface
Alpha = 0.05f Alpha = 0.05f
}; };
colourContainer.Add(flash); ColourContainer.Add(flash);
flash.FadeOutFromOne(100).Expire(); flash.FadeOutFromOne(100).Expire();
clickAnimating = true; clickAnimating = true;
colourContainer.ResizeWidthTo(colourContainer.Width * 1.05f, 100, Easing.OutQuint) ColourContainer.ResizeWidthTo(ColourContainer.Width * 1.05f, 100, Easing.OutQuint)
.OnComplete(_ => .OnComplete(_ =>
{ {
clickAnimating = false; clickAnimating = false;
@ -246,14 +247,14 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
colourContainer.ResizeWidthTo(hover_width * 0.98f, click_duration * 4, Easing.OutQuad); ColourContainer.ResizeWidthTo(hover_width * 0.98f, click_duration * 4, Easing.OutQuad);
return base.OnMouseDown(e); return base.OnMouseDown(e);
} }
protected override void OnMouseUp(MouseUpEvent e) protected override void OnMouseUp(MouseUpEvent e)
{ {
if (State == SelectionState.Selected) if (State == SelectionState.Selected)
colourContainer.ResizeWidthTo(hover_width, click_duration, Easing.In); ColourContainer.ResizeWidthTo(hover_width, click_duration, Easing.In);
base.OnMouseUp(e); base.OnMouseUp(e);
} }
@ -279,12 +280,12 @@ namespace osu.Game.Graphics.UserInterface
if (newState == SelectionState.Selected) if (newState == SelectionState.Selected)
{ {
spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic); spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic);
colourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutElastic); ColourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutElastic);
glowContainer.FadeIn(hover_duration, Easing.OutQuint); glowContainer.FadeIn(hover_duration, Easing.OutQuint);
} }
else else
{ {
colourContainer.ResizeWidthTo(idle_width, hover_duration, Easing.OutElastic); ColourContainer.ResizeWidthTo(idle_width, hover_duration, Easing.OutElastic);
spriteText.TransformSpacingTo(Vector2.Zero, hover_duration, Easing.OutElastic); spriteText.TransformSpacingTo(Vector2.Zero, hover_duration, Easing.OutElastic);
glowContainer.FadeOut(hover_duration, Easing.OutQuint); glowContainer.FadeOut(hover_duration, Easing.OutQuint);
} }

View File

@ -70,9 +70,9 @@ namespace osu.Game.IO
public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) => public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) =>
UnderlyingStorage.GetStream(MutatePath(path), access, mode); UnderlyingStorage.GetStream(MutatePath(path), access, mode);
public override void OpenFileExternally(string filename) => UnderlyingStorage.OpenFileExternally(MutatePath(filename)); public override bool OpenFileExternally(string filename) => UnderlyingStorage.OpenFileExternally(MutatePath(filename));
public override void PresentFileExternally(string filename) => UnderlyingStorage.PresentFileExternally(MutatePath(filename)); public override bool PresentFileExternally(string filename) => UnderlyingStorage.PresentFileExternally(MutatePath(filename));
public override Storage GetStorageForDirectory(string path) public override Storage GetStorageForDirectory(string path)
{ {

View File

@ -8,10 +8,10 @@ using Humanizer;
using MessagePack; using MessagePack;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
namespace osu.Game.Online.API namespace osu.Game.Online.API
{ {
@ -42,7 +42,7 @@ namespace osu.Game.Online.API
var bindable = (IBindable)property.GetValue(mod); var bindable = (IBindable)property.GetValue(mod);
if (!bindable.IsDefault) if (!bindable.IsDefault)
Settings.Add(property.Name.Underscore(), ModUtils.GetSettingUnderlyingValue(bindable)); Settings.Add(property.Name.Underscore(), bindable.GetUnderlyingSettingValue());
} }
} }
@ -51,7 +51,10 @@ namespace osu.Game.Online.API
Mod resultMod = ruleset.CreateModFromAcronym(Acronym); Mod resultMod = ruleset.CreateModFromAcronym(Acronym);
if (resultMod == null) if (resultMod == null)
throw new InvalidOperationException($"There is no mod in the ruleset ({ruleset.ShortName}) matching the acronym {Acronym}."); {
Logger.Log($"There is no mod in the ruleset ({ruleset.ShortName}) matching the acronym {Acronym}.");
return new UnknownMod(Acronym);
}
if (Settings.Count > 0) if (Settings.Count > 0)
{ {
@ -89,13 +92,13 @@ namespace osu.Game.Online.API
public bool Equals(KeyValuePair<string, object> x, KeyValuePair<string, object> y) public bool Equals(KeyValuePair<string, object> x, KeyValuePair<string, object> y)
{ {
object xValue = ModUtils.GetSettingUnderlyingValue(x.Value); object xValue = x.Value.GetUnderlyingSettingValue();
object yValue = ModUtils.GetSettingUnderlyingValue(y.Value); object yValue = y.Value.GetUnderlyingSettingValue();
return x.Key == y.Key && EqualityComparer<object>.Default.Equals(xValue, yValue); return x.Key == y.Key && EqualityComparer<object>.Default.Equals(xValue, yValue);
} }
public int GetHashCode(KeyValuePair<string, object> obj) => HashCode.Combine(obj.Key, ModUtils.GetSettingUnderlyingValue(obj.Value)); public int GetHashCode(KeyValuePair<string, object> obj) => HashCode.Combine(obj.Key, obj.Value.GetUnderlyingSettingValue());
} }
} }
} }

View File

@ -6,7 +6,7 @@ using System.Collections.Generic;
using System.Text; using System.Text;
using MessagePack; using MessagePack;
using MessagePack.Formatters; using MessagePack.Formatters;
using osu.Game.Utils; using osu.Game.Configuration;
namespace osu.Game.Online.API namespace osu.Game.Online.API
{ {
@ -23,7 +23,7 @@ namespace osu.Game.Online.API
var stringBytes = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(kvp.Key)); var stringBytes = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(kvp.Key));
writer.WriteString(in stringBytes); writer.WriteString(in stringBytes);
primitiveFormatter.Serialize(ref writer, ModUtils.GetSettingUnderlyingValue(kvp.Value), options); primitiveFormatter.Serialize(ref writer, kvp.Value.GetUnderlyingSettingValue(), options);
} }
} }

View File

@ -9,6 +9,7 @@ using Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Lists; using osu.Framework.Lists;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Chat;
namespace osu.Game.Online.Chat namespace osu.Game.Online.Chat
{ {
@ -89,6 +90,12 @@ namespace osu.Game.Online.Chat
/// </summary> /// </summary>
public Bindable<bool> Joined = new Bindable<bool>(); public Bindable<bool> Joined = new Bindable<bool>();
/// <summary>
/// Signals if there is a message to highlight.
/// This is automatically cleared by the associated <see cref="DrawableChannel"/> after highlighting.
/// </summary>
public Bindable<Message> HighlightedMessage = new Bindable<Message>();
[JsonConstructor] [JsonConstructor]
public Channel() public Channel()
{ {

View File

@ -59,7 +59,13 @@ namespace osu.Game.Online.Chat
return Id.Value.CompareTo(other.Id.Value); return Id.Value.CompareTo(other.Id.Value);
} }
public virtual bool Equals(Message other) => Id.HasValue && Id == other?.Id; public virtual bool Equals(Message other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Id.HasValue && Id == other.Id;
}
// ReSharper disable once ImpureMethodCallOnReadonlyValueField // ReSharper disable once ImpureMethodCallOnReadonlyValueField
public override int GetHashCode() => Id.GetHashCode(); public override int GetHashCode() => Id.GetHashCode();

View File

@ -114,7 +114,7 @@ namespace osu.Game.Online.Chat
if (!notifyOnPrivateMessage.Value || channel.Type != ChannelType.PM) if (!notifyOnPrivateMessage.Value || channel.Type != ChannelType.PM)
return false; return false;
notifications.Post(new PrivateMessageNotification(message.Sender.Username, channel)); notifications.Post(new PrivateMessageNotification(message, channel));
return true; return true;
} }
@ -122,7 +122,7 @@ namespace osu.Game.Online.Chat
{ {
if (!notifyOnUsername.Value || !CheckContainsUsername(message.Content, localUser.Value.Username)) return; if (!notifyOnUsername.Value || !CheckContainsUsername(message.Content, localUser.Value.Username)) return;
notifications.Post(new MentionNotification(message.Sender.Username, channel)); notifications.Post(new MentionNotification(message, channel));
} }
/// <summary> /// <summary>
@ -136,47 +136,49 @@ namespace osu.Game.Online.Chat
return Regex.IsMatch(message, $@"(^|\W)({fullName}|{underscoreName})($|\W)", RegexOptions.IgnoreCase); return Regex.IsMatch(message, $@"(^|\W)({fullName}|{underscoreName})($|\W)", RegexOptions.IgnoreCase);
} }
public class PrivateMessageNotification : OpenChannelNotification public class PrivateMessageNotification : HighlightMessageNotification
{ {
public PrivateMessageNotification(string username, Channel channel) public PrivateMessageNotification(Message message, Channel channel)
: base(channel) : base(message, channel)
{ {
Icon = FontAwesome.Solid.Envelope; Icon = FontAwesome.Solid.Envelope;
Text = $"You received a private message from '{username}'. Click to read it!"; Text = $"You received a private message from '{message.Sender.Username}'. Click to read it!";
} }
} }
public class MentionNotification : OpenChannelNotification public class MentionNotification : HighlightMessageNotification
{ {
public MentionNotification(string username, Channel channel) public MentionNotification(Message message, Channel channel)
: base(channel) : base(message, channel)
{ {
Icon = FontAwesome.Solid.At; Icon = FontAwesome.Solid.At;
Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!"; Text = $"Your name was mentioned in chat by '{message.Sender.Username}'. Click to find out why!";
} }
} }
public abstract class OpenChannelNotification : SimpleNotification public abstract class HighlightMessageNotification : SimpleNotification
{ {
protected OpenChannelNotification(Channel channel) protected HighlightMessageNotification(Message message, Channel channel)
{ {
this.message = message;
this.channel = channel; this.channel = channel;
} }
private readonly Message message;
private readonly Channel channel; private readonly Channel channel;
public override bool IsImportant => false; public override bool IsImportant => false;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours, ChatOverlay chatOverlay, NotificationOverlay notificationOverlay, ChannelManager channelManager) private void load(OsuColour colours, ChatOverlay chatOverlay, NotificationOverlay notificationOverlay)
{ {
IconBackground.Colour = colours.PurpleDark; IconBackground.Colour = colours.PurpleDark;
Activated = delegate Activated = delegate
{ {
notificationOverlay.Hide(); notificationOverlay.Hide();
chatOverlay.HighlightMessage(message, channel);
chatOverlay.Show(); chatOverlay.Show();
channelManager.CurrentChannel.Value = channel;
return true; return true;
}; };

View File

@ -0,0 +1,22 @@
// 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 enable
using MessagePack;
namespace osu.Game.Online.Multiplayer.Countdown
{
/// <summary>
/// Indicates a change to the <see cref="MultiplayerRoom"/>'s countdown.
/// </summary>
[MessagePackObject]
public class CountdownChangedEvent : MatchServerEvent
{
/// <summary>
/// The new countdown.
/// </summary>
[Key(0)]
public MultiplayerCountdown? Countdown { get; set; }
}
}

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