1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-28 02:43:19 +08:00

Merge remote-tracking branch 'upstream/master' into back-button-part-2

This commit is contained in:
David Zhao 2019-08-13 12:26:12 +09:00
commit 4a28bdd384
140 changed files with 3470 additions and 1099 deletions

0
.gitmodules vendored
View File

View File

@ -1,2 +0,0 @@
language: csharp
solution: osu.sln

View File

@ -34,7 +34,7 @@ If you are not interested in developing the game, you can still consume our [bin
| ------------- | ------------- |
- **Linux** users are recommended to self-compile until we have official deployment in place.
- **iOS** users can join the [TestFlight beta program](https://t.co/PasE1zrHhw) (note that due to high demand this is regularly full).
- **iOS** users can join the [TestFlight beta program](https://testflight.apple.com/join/2tLcjWlF) (note that due to high demand this is regularly full).
- **Android** users can self-compile, and expect a public beta soon.
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.

View File

@ -5,7 +5,7 @@
<TargetFrameworks>netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Cake" Version="0.30.0" />
<PackageReference Include="Cake.CoreCLR" Version="0.30.0" />
<PackageReference Include="Cake" Version="0.34.1" />
<PackageReference Include="Cake.CoreCLR" Version="0.34.1" />
</ItemGroup>
</Project>

View File

@ -62,7 +62,7 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.702.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.730.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.809.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.809.0" />
</ItemGroup>
</Project>

View File

@ -28,8 +28,8 @@
<ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="4.5.0" />
<PackageReference Include="ppy.squirrel.windows" Version="1.9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.6" />
</ItemGroup>
<ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" />

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using Newtonsoft.Json;
using NUnit.Framework;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
@ -22,10 +23,10 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestCase("spinner")]
[TestCase("spinner-and-circles")]
[TestCase("slider")]
public new void Test(string name)
{
base.Test(name);
}
[TestCase("hardrock-stream", new[] { typeof(CatchModHardRock) })]
[TestCase("hardrock-repeat-slider", new[] { typeof(CatchModHardRock) })]
[TestCase("hardrock-spinner", new[] { typeof(CatchModHardRock) })]
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
{

View File

@ -4,7 +4,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.14.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">

View File

@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
using osu.Game.Rulesets.Catch.MathUtils;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Beatmaps
{
@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
base.PostProcess();
applyPositionOffsets();
ApplyPositionOffsets(Beatmap);
initialiseHyperDash((List<CatchHitObject>)Beatmap.HitObjects);
@ -40,19 +41,29 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
}
}
private void applyPositionOffsets()
public static void ApplyPositionOffsets(IBeatmap beatmap, params Mod[] mods)
{
var rng = new FastRandom(RNG_SEED);
// todo: HardRock displacement should be applied here
foreach (var obj in Beatmap.HitObjects)
bool shouldApplyHardRockOffset = mods.Any(m => m is ModHardRock);
float? lastPosition = null;
double lastStartTime = 0;
foreach (var obj in beatmap.HitObjects.OfType<CatchHitObject>())
{
obj.XOffset = 0;
switch (obj)
{
case Fruit fruit:
if (shouldApplyHardRockOffset)
applyHardRockOffset(fruit, ref lastPosition, ref lastStartTime, rng);
break;
case BananaShower bananaShower:
foreach (var banana in bananaShower.NestedHitObjects.OfType<Banana>())
{
banana.X = (float)rng.NextDouble();
banana.XOffset = (float)rng.NextDouble();
rng.Next(); // osu!stable retrieved a random banana type
rng.Next(); // osu!stable retrieved a random banana rotation
rng.Next(); // osu!stable retrieved a random banana colour
@ -63,12 +74,13 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
case JuiceStream juiceStream:
foreach (var nested in juiceStream.NestedHitObjects)
{
var hitObject = (CatchHitObject)nested;
if (hitObject is TinyDroplet)
hitObject.X += rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH;
else if (hitObject is Droplet)
var catchObject = (CatchHitObject)nested;
catchObject.XOffset = 0;
if (catchObject is TinyDroplet)
catchObject.XOffset = MathHelper.Clamp(rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH, -catchObject.X, 1 - catchObject.X);
else if (catchObject is Droplet)
rng.Next(); // osu!stable retrieved a random droplet rotation
hitObject.X = MathHelper.Clamp(hitObject.X, 0, 1);
}
break;
@ -76,6 +88,105 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
}
}
private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng)
{
if (hitObject is JuiceStream stream)
{
lastPosition = stream.EndX;
lastStartTime = stream.EndTime;
return;
}
if (!(hitObject is Fruit))
return;
float offsetPosition = hitObject.X;
double startTime = hitObject.StartTime;
if (lastPosition == null)
{
lastPosition = offsetPosition;
lastStartTime = startTime;
return;
}
float positionDiff = offsetPosition - lastPosition.Value;
double timeDiff = startTime - lastStartTime;
if (timeDiff > 1000)
{
lastPosition = offsetPosition;
lastStartTime = startTime;
return;
}
if (positionDiff == 0)
{
applyRandomOffset(ref offsetPosition, timeDiff / 4d, rng);
hitObject.XOffset = offsetPosition - hitObject.X;
return;
}
if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3d)
applyOffset(ref offsetPosition, positionDiff);
hitObject.XOffset = offsetPosition - hitObject.X;
lastPosition = offsetPosition;
lastStartTime = startTime;
}
/// <summary>
/// Applies a random offset in a random direction to a position, ensuring that the final position remains within the boundary of the playfield.
/// </summary>
/// <param name="position">The position which the offset should be applied to.</param>
/// <param name="maxOffset">The maximum offset, cannot exceed 20px.</param>
/// <param name="rng">The random number generator.</param>
private static void applyRandomOffset(ref float position, double maxOffset, FastRandom rng)
{
bool right = rng.NextBool();
float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset))) / CatchPlayfield.BASE_WIDTH;
if (right)
{
// Clamp to the right bound
if (position + rand <= 1)
position += rand;
else
position -= rand;
}
else
{
// Clamp to the left bound
if (position - rand >= 0)
position -= rand;
else
position += rand;
}
}
/// <summary>
/// Applies an offset to a position, ensuring that the final position remains within the boundary of the playfield.
/// </summary>
/// <param name="position">The position which the offset should be applied to.</param>
/// <param name="amount">The amount to offset by.</param>
private static void applyOffset(ref float position, float amount)
{
if (amount > 0)
{
// Clamp to the right bound
if (position + amount < 1)
position += amount;
}
else
{
// Clamp to the left bound
if (position + amount > 0)
position += amount;
}
}
private void initialiseHyperDash(List<CatchHitObject> objects)
{
List<CatchHitObject> objectWithDroplets = new List<CatchHitObject>();

View File

@ -11,7 +11,6 @@ using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy;
@ -108,7 +107,7 @@ namespace osu.Game.Rulesets.Catch
case ModType.Fun:
return new Mod[]
{
new MultiMod(new ModWindUp<CatchHitObject>(), new ModWindDown<CatchHitObject>())
new MultiMod(new ModWindUp(), new ModWindDown())
};
default:

View File

@ -61,6 +61,14 @@ namespace osu.Game.Rulesets.Catch.MathUtils
/// <returns>The random value.</returns>
public int Next(int lowerBound, int upperBound) => (int)(lowerBound + NextDouble() * (upperBound - lowerBound));
/// <summary>
/// Generates a random integer value within the range [<paramref name="lowerBound"/>, <paramref name="upperBound"/>).
/// </summary>
/// <param name="lowerBound">The lower bound of the range.</param>
/// <param name="upperBound">The upper bound of the range.</param>
/// <returns>The random value.</returns>
public int Next(double lowerBound, double upperBound) => (int)(lowerBound + NextDouble() * (upperBound - lowerBound));
/// <summary>
/// Generates a random double value within the range [0, 1).
/// </summary>

View File

@ -1,121 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
using System;
using osu.Game.Rulesets.Objects;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Beatmaps;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModHardRock : ModHardRock, IApplicableToHitObject
public class CatchModHardRock : ModHardRock, IApplicableToBeatmap
{
public override double ScoreMultiplier => 1.12;
public override bool Ranked => true;
private float? lastPosition;
private double lastStartTime;
public void ApplyToHitObject(HitObject hitObject)
{
if (hitObject is JuiceStream stream)
{
lastPosition = stream.EndX;
lastStartTime = stream.EndTime;
return;
}
if (!(hitObject is Fruit))
return;
var catchObject = (CatchHitObject)hitObject;
float position = catchObject.X;
double startTime = hitObject.StartTime;
if (lastPosition == null)
{
lastPosition = position;
lastStartTime = startTime;
return;
}
float positionDiff = position - lastPosition.Value;
double timeDiff = startTime - lastStartTime;
if (timeDiff > 1000)
{
lastPosition = position;
lastStartTime = startTime;
return;
}
if (positionDiff == 0)
{
applyRandomOffset(ref position, timeDiff / 4d);
catchObject.X = position;
return;
}
if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3d)
applyOffset(ref position, positionDiff);
catchObject.X = position;
lastPosition = position;
lastStartTime = startTime;
}
/// <summary>
/// Applies a random offset in a random direction to a position, ensuring that the final position remains within the boundary of the playfield.
/// </summary>
/// <param name="position">The position which the offset should be applied to.</param>
/// <param name="maxOffset">The maximum offset, cannot exceed 20px.</param>
private void applyRandomOffset(ref float position, double maxOffset)
{
bool right = RNG.NextBool();
float rand = Math.Min(20, (float)RNG.NextDouble(0, Math.Max(0, maxOffset))) / CatchPlayfield.BASE_WIDTH;
if (right)
{
// Clamp to the right bound
if (position + rand <= 1)
position += rand;
else
position -= rand;
}
else
{
// Clamp to the left bound
if (position - rand >= 0)
position -= rand;
else
position += rand;
}
}
/// <summary>
/// Applies an offset to a position, ensuring that the final position remains within the boundary of the playfield.
/// </summary>
/// <param name="position">The position which the offset should be applied to.</param>
/// <param name="amount">The amount to offset by.</param>
private void applyOffset(ref float position, float amount)
{
if (amount > 0)
{
// Clamp to the right bound
if (position + amount < 1)
position += amount;
}
else
{
// Clamp to the left bound
if (position + amount > 0)
position += amount;
}
}
public void ApplyToBeatmap(IBeatmap beatmap) => CatchBeatmapProcessor.ApplyPositionOffsets(beatmap, this);
}
}

View File

@ -3,6 +3,7 @@
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@ -12,7 +13,18 @@ namespace osu.Game.Rulesets.Catch.Objects
{
public const double OBJECT_RADIUS = 44;
public float X { get; set; }
private float x;
public float X
{
get => x + XOffset;
set => x = value;
}
/// <summary>
/// A random offset applied to <see cref="X"/>, set by the <see cref="CatchBeatmapProcessor"/>.
/// </summary>
internal float XOffset { get; set; }
public double TimePreempt = 1000;

View File

@ -0,0 +1,150 @@
{
"Mappings": [{
"StartTime": 369,
"Objects": [{
"StartTime": 369,
"Position": 177
},
{
"StartTime": 450,
"Position": 216.539276
},
{
"StartTime": 532,
"Position": 256.5667
},
{
"StartTime": 614,
"Position": 296.594116
},
{
"StartTime": 696,
"Position": 336.621521
},
{
"StartTime": 778,
"Position": 376.99762
},
{
"StartTime": 860,
"Position": 337.318878
},
{
"StartTime": 942,
"Position": 297.291443
},
{
"StartTime": 1024,
"Position": 257.264038
},
{
"StartTime": 1106,
"Position": 217.2366
},
{
"StartTime": 1188,
"Position": 177
},
{
"StartTime": 1270,
"Position": 216.818192
},
{
"StartTime": 1352,
"Position": 256.8456
},
{
"StartTime": 1434,
"Position": 296.873047
},
{
"StartTime": 1516,
"Position": 336.900452
},
{
"StartTime": 1598,
"Position": 376.99762
},
{
"StartTime": 1680,
"Position": 337.039948
},
{
"StartTime": 1762,
"Position": 297.0125
},
{
"StartTime": 1844,
"Position": 256.9851
},
{
"StartTime": 1926,
"Position": 216.957672
},
{
"StartTime": 2008,
"Position": 177
},
{
"StartTime": 2090,
"Position": 217.097137
},
{
"StartTime": 2172,
"Position": 257.124573
},
{
"StartTime": 2254,
"Position": 297.152
},
{
"StartTime": 2336,
"Position": 337.179443
},
{
"StartTime": 2418,
"Position": 376.99762
},
{
"StartTime": 2500,
"Position": 336.760956
},
{
"StartTime": 2582,
"Position": 296.733643
},
{
"StartTime": 2664,
"Position": 256.7062
},
{
"StartTime": 2746,
"Position": 216.678772
},
{
"StartTime": 2828,
"Position": 177
},
{
"StartTime": 2909,
"Position": 216.887909
},
{
"StartTime": 2991,
"Position": 256.915344
},
{
"StartTime": 3073,
"Position": 296.942749
},
{
"StartTime": 3155,
"Position": 336.970184
},
{
"StartTime": 3237,
"Position": 376.99762
}
]
}]
}

View File

@ -0,0 +1,18 @@
osu file format v14
[General]
StackLeniency: 0.4
Mode: 0
[Difficulty]
CircleSize:4
OverallDifficulty:7
ApproachRate:8
SliderMultiplier:1.6
SliderTickRate:4
[TimingPoints]
369,327.868852459016,4,2,2,32,1,0
[HitObjects]
177,191,369,6,0,L|382:192,7,200

View File

@ -0,0 +1,74 @@
{
"Mappings": [{
"StartTime": 369,
"Objects": [{
"StartTime": 369,
"Position": 65
},
{
"StartTime": 450,
"Position": 482
},
{
"StartTime": 532,
"Position": 164
},
{
"StartTime": 614,
"Position": 315
},
{
"StartTime": 696,
"Position": 145
},
{
"StartTime": 778,
"Position": 159
},
{
"StartTime": 860,
"Position": 310
},
{
"StartTime": 942,
"Position": 441
},
{
"StartTime": 1024,
"Position": 428
},
{
"StartTime": 1106,
"Position": 243
},
{
"StartTime": 1188,
"Position": 422
},
{
"StartTime": 1270,
"Position": 481
},
{
"StartTime": 1352,
"Position": 104
},
{
"StartTime": 1434,
"Position": 473
},
{
"StartTime": 1516,
"Position": 135
},
{
"StartTime": 1598,
"Position": 360
},
{
"StartTime": 1680,
"Position": 123
}
]
}]
}

View File

@ -0,0 +1,18 @@
osu file format v14
[General]
StackLeniency: 0.4
Mode: 0
[Difficulty]
CircleSize:4
OverallDifficulty:7
ApproachRate:8
SliderMultiplier:1.6
SliderTickRate:4
[TimingPoints]
369,327.868852459016,4,2,2,32,1,0
[HitObjects]
256,192,369,12,0,1680,0:0:0:0:

View File

@ -0,0 +1,234 @@
{
"Mappings": [{
"StartTime": 369,
"Objects": [{
"StartTime": 369,
"Position": 258
}]
},
{
"StartTime": 450,
"Objects": [{
"StartTime": 450,
"Position": 254
}]
},
{
"StartTime": 532,
"Objects": [{
"StartTime": 532,
"Position": 241
}]
},
{
"StartTime": 614,
"Objects": [{
"StartTime": 614,
"Position": 238
}]
},
{
"StartTime": 696,
"Objects": [{
"StartTime": 696,
"Position": 238
}]
},
{
"StartTime": 778,
"Objects": [{
"StartTime": 778,
"Position": 278
}]
},
{
"StartTime": 860,
"Objects": [{
"StartTime": 860,
"Position": 238
}]
},
{
"StartTime": 942,
"Objects": [{
"StartTime": 942,
"Position": 278
}]
},
{
"StartTime": 1024,
"Objects": [{
"StartTime": 1024,
"Position": 238
}]
},
{
"StartTime": 1106,
"Objects": [{
"StartTime": 1106,
"Position": 278
}]
},
{
"StartTime": 1188,
"Objects": [{
"StartTime": 1188,
"Position": 278
}]
},
{
"StartTime": 1270,
"Objects": [{
"StartTime": 1270,
"Position": 278
}]
},
{
"StartTime": 1352,
"Objects": [{
"StartTime": 1352,
"Position": 238
}]
},
{
"StartTime": 1434,
"Objects": [{
"StartTime": 1434,
"Position": 258
}]
},
{
"StartTime": 1516,
"Objects": [{
"StartTime": 1516,
"Position": 253
}]
},
{
"StartTime": 1598,
"Objects": [{
"StartTime": 1598,
"Position": 238
}]
},
{
"StartTime": 1680,
"Objects": [{
"StartTime": 1680,
"Position": 260
}]
},
{
"StartTime": 1762,
"Objects": [{
"StartTime": 1762,
"Position": 238
}]
},
{
"StartTime": 1844,
"Objects": [{
"StartTime": 1844,
"Position": 278
}]
},
{
"StartTime": 1926,
"Objects": [{
"StartTime": 1926,
"Position": 278
}]
},
{
"StartTime": 2008,
"Objects": [{
"StartTime": 2008,
"Position": 238
}]
},
{
"StartTime": 2090,
"Objects": [{
"StartTime": 2090,
"Position": 238
}]
},
{
"StartTime": 2172,
"Objects": [{
"StartTime": 2172,
"Position": 243
}]
},
{
"StartTime": 2254,
"Objects": [{
"StartTime": 2254,
"Position": 278
}]
},
{
"StartTime": 2336,
"Objects": [{
"StartTime": 2336,
"Position": 278
}]
},
{
"StartTime": 2418,
"Objects": [{
"StartTime": 2418,
"Position": 238
}]
},
{
"StartTime": 2500,
"Objects": [{
"StartTime": 2500,
"Position": 258
}]
},
{
"StartTime": 2582,
"Objects": [{
"StartTime": 2582,
"Position": 256
}]
},
{
"StartTime": 2664,
"Objects": [{
"StartTime": 2664,
"Position": 242
}]
},
{
"StartTime": 2746,
"Objects": [{
"StartTime": 2746,
"Position": 238
}]
},
{
"StartTime": 2828,
"Objects": [{
"StartTime": 2828,
"Position": 238
}]
},
{
"StartTime": 2909,
"Objects": [{
"StartTime": 2909,
"Position": 271
}]
},
{
"StartTime": 2991,
"Objects": [{
"StartTime": 2991,
"Position": 254
}]
}
]
}

View File

@ -0,0 +1,50 @@
osu file format v14
[General]
StackLeniency: 0.4
Mode: 0
[Difficulty]
CircleSize:4
OverallDifficulty:7
ApproachRate:8
SliderMultiplier:1.6
SliderTickRate:1
[TimingPoints]
369,327.868852459016,4,2,2,32,1,0
[HitObjects]
258,189,369,1,0,0:0:0:0:
258,189,450,1,0,0:0:0:0:
258,189,532,1,0,0:0:0:0:
258,189,614,1,0,0:0:0:0:
258,189,696,1,0,0:0:0:0:
258,189,778,1,0,0:0:0:0:
258,189,860,1,0,0:0:0:0:
258,189,942,1,0,0:0:0:0:
258,189,1024,1,0,0:0:0:0:
258,189,1106,1,0,0:0:0:0:
258,189,1188,1,0,0:0:0:0:
258,189,1270,1,0,0:0:0:0:
258,189,1352,1,0,0:0:0:0:
258,189,1434,1,0,0:0:0:0:
258,189,1516,1,0,0:0:0:0:
258,189,1598,1,0,0:0:0:0:
258,189,1680,1,0,0:0:0:0:
258,189,1762,1,0,0:0:0:0:
258,189,1844,1,0,0:0:0:0:
258,189,1926,1,0,0:0:0:0:
258,189,2008,1,0,0:0:0:0:
258,189,2090,1,0,0:0:0:0:
258,189,2172,1,0,0:0:0:0:
258,189,2254,1,0,0:0:0:0:
258,189,2336,1,0,0:0:0:0:
258,189,2418,1,0,0:0:0:0:
258,189,2500,1,0,0:0:0:0:
258,189,2582,1,0,0:0:0:0:
258,189,2664,1,0,0:0:0:0:
258,189,2746,1,0,0:0:0:0:
258,189,2828,1,0,0:0:0:0:
258,189,2909,1,0,0:0:0:0:
258,189,2991,1,0,0:0:0:0:

View File

@ -20,10 +20,7 @@ namespace osu.Game.Rulesets.Mania.Tests
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase("basic")]
public new void Test(string name)
{
base.Test(name);
}
public void Test(string name) => base.Test(name);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
{
@ -35,11 +32,37 @@ namespace osu.Game.Rulesets.Mania.Tests
};
}
protected override ManiaConvertMapping CreateConvertMapping() => new ManiaConvertMapping(Converter);
private readonly Dictionary<HitObject, RngSnapshot> rngSnapshots = new Dictionary<HitObject, RngSnapshot>();
protected override void OnConversionGenerated(HitObject original, IEnumerable<HitObject> result, IBeatmapConverter beatmapConverter)
{
base.OnConversionGenerated(original, result, beatmapConverter);
rngSnapshots[original] = new RngSnapshot(beatmapConverter);
}
protected override ManiaConvertMapping CreateConvertMapping(HitObject source) => new ManiaConvertMapping(rngSnapshots[source]);
protected override Ruleset CreateRuleset() => new ManiaRuleset();
}
public class RngSnapshot
{
public readonly uint RandomW;
public readonly uint RandomX;
public readonly uint RandomY;
public readonly uint RandomZ;
public RngSnapshot(IBeatmapConverter converter)
{
var maniaConverter = (ManiaBeatmapConverter)converter;
RandomW = maniaConverter.Random.W;
RandomX = maniaConverter.Random.X;
RandomY = maniaConverter.Random.Y;
RandomZ = maniaConverter.Random.Z;
}
}
public class ManiaConvertMapping : ConvertMapping<ConvertValue>, IEquatable<ManiaConvertMapping>
{
public uint RandomW;
@ -51,13 +74,12 @@ namespace osu.Game.Rulesets.Mania.Tests
{
}
public ManiaConvertMapping(IBeatmapConverter converter)
public ManiaConvertMapping(RngSnapshot snapshot)
{
var maniaConverter = (ManiaBeatmapConverter)converter;
RandomW = maniaConverter.Random.W;
RandomX = maniaConverter.Random.X;
RandomY = maniaConverter.Random.Y;
RandomZ = maniaConverter.Random.Z;
RandomW = snapshot.RandomW;
RandomX = snapshot.RandomX;
RandomY = snapshot.RandomY;
RandomZ = snapshot.RandomZ;
}
public bool Equals(ManiaConvertMapping other) => other != null && RandomW == other.RandomW && RandomX == other.RandomX && RandomY == other.RandomY && RandomZ == other.RandomZ;

View File

@ -4,7 +4,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.14.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <summary>
/// Keep the same as last row.
/// </summary>
ForceStack = 1 << 0,
ForceStack = 1,
/// <summary>
/// Keep different from last row.

View File

@ -13,7 +13,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy;
@ -154,7 +153,7 @@ namespace osu.Game.Rulesets.Mania
case ModType.Fun:
return new Mod[]
{
new MultiMod(new ModWindUp<ManiaHitObject>(), new ModWindDown<ManiaHitObject>())
new MultiMod(new ModWindUp(), new ModWindDown())
};
default:

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mania.Beatmaps;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModMirror : Mod, IApplicableToBeatmap<ManiaHitObject>
public class ManiaModMirror : Mod, IApplicableToBeatmap
{
public override string Name => "Mirror";
public override string Acronym => "MR";
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override double ScoreMultiplier => 1;
public override bool Ranked => true;
public void ApplyToBeatmap(Beatmap<ManiaHitObject> beatmap)
public void ApplyToBeatmap(IBeatmap beatmap)
{
var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;

View File

@ -13,7 +13,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModRandom : Mod, IApplicableToBeatmap<ManiaHitObject>
public class ManiaModRandom : Mod, IApplicableToBeatmap
{
public override string Name => "Random";
public override string Acronym => "RD";
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Description => @"Shuffle around the keys!";
public override double ScoreMultiplier => 1;
public void ApplyToBeatmap(Beatmap<ManiaHitObject> beatmap)
public void ApplyToBeatmap(IBeatmap beatmap)
{
var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(item => RNG.Next()).ToList();

View File

@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
}
}
private Cached subtractionCache = new Cached();
private readonly Cached subtractionCache = new Cached();
public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true)
{

View File

@ -8,7 +8,6 @@ using osu.Framework.MathUtils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Osu.Tests
@ -21,10 +20,10 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase("basic")]
[TestCase("colinear-perfect-curve")]
[TestCase("slider-ticks")]
public new void Test(string name)
{
base.Test(name);
}
[TestCase("repeat-slider")]
[TestCase("uneven-repeat-slider")]
[TestCase("old-stacking")]
public void Test(string name) => base.Test(name);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
{
@ -32,22 +31,22 @@ namespace osu.Game.Rulesets.Osu.Tests
{
case Slider slider:
foreach (var nested in slider.NestedHitObjects)
yield return createConvertValue(nested);
yield return createConvertValue((OsuHitObject)nested);
break;
default:
yield return createConvertValue(hitObject);
yield return createConvertValue((OsuHitObject)hitObject);
break;
}
ConvertValue createConvertValue(HitObject obj) => new ConvertValue
ConvertValue createConvertValue(OsuHitObject obj) => new ConvertValue
{
StartTime = obj.StartTime,
EndTime = (obj as IHasEndTime)?.EndTime ?? obj.StartTime,
X = (obj as IHasPosition)?.X ?? OsuPlayfield.BASE_SIZE.X / 2,
Y = (obj as IHasPosition)?.Y ?? OsuPlayfield.BASE_SIZE.Y / 2,
X = obj.StackedPosition.X,
Y = obj.StackedPosition.Y
};
}

View File

@ -4,7 +4,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.14.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">

View File

@ -6,6 +6,7 @@ using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Beatmaps
{
@ -208,17 +209,22 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
if (beatmap.HitObjects[j].StartTime - stackThreshold > startTime)
break;
// The start position of the hitobject, or the position at the end of the path if the hitobject is a slider
Vector2 position2 = currHitObject is Slider currSlider
? currSlider.Position + currSlider.Path.PositionAt(1)
: currHitObject.Position;
if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance)
{
currHitObject.StackHeight++;
startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[i].StartTime;
startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[j].StartTime;
}
else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.EndPosition) < stack_distance)
else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance)
{
//Case for sliders - bump notes down and right, rather than up and left.
sliderStack++;
beatmap.HitObjects[j].StackHeight -= sliderStack;
startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[i].StartTime;
startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[j].StartTime;
}
}
}

View File

@ -2,13 +2,19 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.StateChanges;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAutopilot : Mod
public class OsuModAutopilot : Mod, IApplicableFailOverride, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => "Autopilot";
public override string Acronym => "AP";
@ -17,5 +23,40 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay) };
public bool AllowFail => false;
private OsuInputManager inputManager;
private List<OsuReplayFrame> replayFrames;
private int currentFrame;
public void Update(Playfield playfield)
{
if (currentFrame == replayFrames.Count - 1) return;
double time = playfield.Time.Current;
// Very naive implementation of autopilot based on proximity to replay frames.
// TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered).
if (Math.Abs(replayFrames[currentFrame + 1].Time - time) <= Math.Abs(replayFrames[currentFrame].Time - time))
{
currentFrame++;
new MousePositionAbsoluteInput { Position = playfield.ToScreenSpace(replayFrames[currentFrame].Position) }.Apply(inputManager.CurrentState, inputManager);
}
// TODO: Implement the functionality to automatically spin spinners
}
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// Grab the input manager to disable the user's cursor, and for future use
inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
inputManager.AllowUserCursorMovement = false;
// Generate the replay frames the cursor should follow
replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap).Generate().Frames.Cast<OsuReplayFrame>().ToList();
}
}
}

View File

@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
var spanProgress = slider.ProgressAt(completionProgress);
double start = 0;
double end = SnakingIn.Value ? MathHelper.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / slider.TimeFadeIn, 0, 1) : 1;
double end = SnakingIn.Value ? MathHelper.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / (slider.TimePreempt / 3), 0, 1) : 1;
if (span >= slider.SpanCount() - 1)
{

View File

@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
TimeFadeIn = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1200, 800, 300);
TimeFadeIn = 400; // as per osu-stable
Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2;
}

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
public double Duration => EndTime - StartTime;
private Cached<Vector2> endPositionCache;
private readonly Cached<Vector2> endPositionCache = new Cached<Vector2>();
public override Vector2 EndPosition => endPositionCache.IsValid ? endPositionCache.Value : endPositionCache.Value = Position + this.CurvePositionAt(1);

View File

@ -18,6 +18,12 @@ namespace osu.Game.Rulesets.Osu
set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowUserPresses = value;
}
/// <summary>
/// Whether the user's cursor movement events should be accepted.
/// Can be used to block only movement while still accepting button input.
/// </summary>
public bool AllowUserCursorMovement { get; set; } = true;
protected override RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
=> new OsuKeyBindingContainer(ruleset, variant, unique);
@ -26,6 +32,13 @@ namespace osu.Game.Rulesets.Osu
{
}
protected override bool Handle(UIEvent e)
{
if (e is MouseMoveEvent && !AllowUserCursorMovement) return false;
return base.Handle(e);
}
private class OsuKeyBindingContainer : RulesetKeyBindingContainer
{
public bool AllowUserPresses = true;

View File

@ -14,7 +14,6 @@ using osu.Game.Overlays.Settings;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy;
@ -136,7 +135,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModWiggle(),
new OsuModSpinIn(),
new MultiMod(new OsuModGrow(), new OsuModDeflate()),
new MultiMod(new ModWindUp<OsuHitObject>(), new ModWindDown<OsuHitObject>()),
new MultiMod(new ModWindUp(), new ModWindDown()),
};
case ModType.System:

View File

@ -0,0 +1,278 @@
{
"Mappings": [{
"StartTime": 32165,
"Objects": [{
"StartTime": 32165,
"EndTime": 32165,
"X": 32,
"Y": 320
}]
},
{
"StartTime": 32517,
"Objects": [{
"StartTime": 32517,
"EndTime": 32517,
"X": 246.396057,
"Y": 182.396057
}]
},
{
"StartTime": 32605,
"Objects": [{
"StartTime": 32605,
"EndTime": 32605,
"X": 249.597382,
"Y": 185.597382
}]
},
{
"StartTime": 32693,
"Objects": [{
"StartTime": 32693,
"EndTime": 32693,
"X": 252.798691,
"Y": 188.798691
}]
},
{
"StartTime": 32781,
"Objects": [{
"StartTime": 32781,
"EndTime": 32781,
"X": 256,
"Y": 192
}]
},
{
"StartTime": 33248,
"Objects": [{
"StartTime": 33248,
"EndTime": 33248,
"X": 39.3960648,
"Y": 76.3960648
}]
},
{
"StartTime": 33307,
"Objects": [{
"StartTime": 33307,
"EndTime": 33307,
"X": 42.5973778,
"Y": 79.597374
}]
},
{
"StartTime": 33383,
"Objects": [{
"StartTime": 33383,
"EndTime": 33383,
"X": 45.798687,
"Y": 82.79869
}]
},
{
"StartTime": 33459,
"Objects": [{
"StartTime": 33459,
"EndTime": 33459,
"X": 49,
"Y": 86
},
{
"StartTime": 33635,
"EndTime": 33635,
"X": 123.847847,
"Y": 85.7988
},
{
"StartTime": 33811,
"EndTime": 33811,
"X": 198.6957,
"Y": 85.5975952
},
{
"StartTime": 33988,
"EndTime": 33988,
"X": 273.9688,
"Y": 85.39525
},
{
"StartTime": 34164,
"EndTime": 34164,
"X": 348.816681,
"Y": 85.19404
},
{
"StartTime": 34246,
"EndTime": 34246,
"X": 398.998718,
"Y": 85.05914
}
]
},
{
"StartTime": 34341,
"Objects": [{
"StartTime": 34341,
"EndTime": 34341,
"X": 401.201324,
"Y": 88.20131
}]
},
{
"StartTime": 34400,
"Objects": [{
"StartTime": 34400,
"EndTime": 34400,
"X": 404.402618,
"Y": 91.402626
}]
},
{
"StartTime": 34459,
"Objects": [{
"StartTime": 34459,
"EndTime": 34459,
"X": 407.603943,
"Y": 94.6039352
}]
},
{
"StartTime": 34989,
"Objects": [{
"StartTime": 34989,
"EndTime": 34989,
"X": 163,
"Y": 138
},
{
"StartTime": 35018,
"EndTime": 35018,
"X": 188,
"Y": 138
}
]
},
{
"StartTime": 35106,
"Objects": [{
"StartTime": 35106,
"EndTime": 35106,
"X": 163,
"Y": 138
},
{
"StartTime": 35135,
"EndTime": 35135,
"X": 188,
"Y": 138
}
]
},
{
"StartTime": 35224,
"Objects": [{
"StartTime": 35224,
"EndTime": 35224,
"X": 163,
"Y": 138
},
{
"StartTime": 35253,
"EndTime": 35253,
"X": 188,
"Y": 138
}
]
},
{
"StartTime": 35695,
"Objects": [{
"StartTime": 35695,
"EndTime": 35695,
"X": 166,
"Y": 76
},
{
"StartTime": 35871,
"EndTime": 35871,
"X": 240.99855,
"Y": 75.53417
},
{
"StartTime": 36011,
"EndTime": 36011,
"X": 315.9971,
"Y": 75.0683441
}
]
},
{
"StartTime": 36106,
"Objects": [{
"StartTime": 36106,
"EndTime": 36106,
"X": 315,
"Y": 75
},
{
"StartTime": 36282,
"EndTime": 36282,
"X": 240.001526,
"Y": 75.47769
},
{
"StartTime": 36422,
"EndTime": 36422,
"X": 165.003052,
"Y": 75.95539
}
]
},
{
"StartTime": 36518,
"Objects": [{
"StartTime": 36518,
"EndTime": 36518,
"X": 166,
"Y": 76
},
{
"StartTime": 36694,
"EndTime": 36694,
"X": 240.99855,
"Y": 75.53417
},
{
"StartTime": 36834,
"EndTime": 36834,
"X": 315.9971,
"Y": 75.0683441
}
]
},
{
"StartTime": 36929,
"Objects": [{
"StartTime": 36929,
"EndTime": 36929,
"X": 315,
"Y": 75
},
{
"StartTime": 37105,
"EndTime": 37105,
"X": 240.001526,
"Y": 75.47769
},
{
"StartTime": 37245,
"EndTime": 37245,
"X": 165.003052,
"Y": 75.95539
}
]
}
]
}

View File

@ -0,0 +1,40 @@
osu file format v3
[Difficulty]
HPDrainRate:3
CircleSize:5
OverallDifficulty:8
ApproachRate:8
SliderMultiplier:1.5
SliderTickRate:2
[TimingPoints]
48,352.941176470588,4,1,1,100,1,0
[HitObjects]
// Hit circles
32,320,32165,5,2,0:0:0:0:
256,192,32517,5,0,0:0:0:0:
256,192,32605,1,0,0:0:0:0:
256,192,32693,1,0,0:0:0:0:
256,192,32781,1,0,0:0:0:0:
// Hit circles on slider endpoints
49,86,33248,1,0,0:0:0:0:
49,86,33307,1,0,0:0:0:0:
49,86,33383,1,0,0:0:0:0:
49,86,33459,2,0,L|421:85,1,350
398,85,34341,1,0,0:0:0:0:
398,85,34400,1,0,0:0:0:0:
398,85,34459,1,0,0:0:0:0:
// Sliders
163,138,34989,2,0,L|196:138,1,25
163,138,35106,2,0,L|196:138,1,25
163,138,35224,2,0,L|196:138,1,25
// Reversed sliders
166,76,35695,2,0,L|327:75,1,150
315,75,36106,2,0,L|158:76,1,150
166,76,36518,2,0,L|327:75,1,150
315,75,36929,2,0,L|158:76,1,150

View File

@ -0,0 +1,222 @@
{
"Mappings": [{
"StartTime": 369,
"Objects": [{
"StartTime": 369,
"EndTime": 369,
"X": 177,
"Y": 191
},
{
"StartTime": 450,
"EndTime": 450,
"X": 216.539276,
"Y": 191.192871
},
{
"StartTime": 532,
"EndTime": 532,
"X": 256.5667,
"Y": 191.388138
},
{
"StartTime": 614,
"EndTime": 614,
"X": 296.594116,
"Y": 191.583389
},
{
"StartTime": 696,
"EndTime": 696,
"X": 336.621521,
"Y": 191.778641
},
{
"StartTime": 778,
"EndTime": 778,
"X": 376.648926,
"Y": 191.9739
},
{
"StartTime": 860,
"EndTime": 860,
"X": 337.318878,
"Y": 191.782043
},
{
"StartTime": 942,
"EndTime": 942,
"X": 297.291443,
"Y": 191.586792
},
{
"StartTime": 1024,
"EndTime": 1024,
"X": 257.264038,
"Y": 191.391541
},
{
"StartTime": 1106,
"EndTime": 1106,
"X": 217.2366,
"Y": 191.196274
},
{
"StartTime": 1188,
"EndTime": 1188,
"X": 177.209213,
"Y": 191.001022
},
{
"StartTime": 1270,
"EndTime": 1270,
"X": 216.818192,
"Y": 191.194229
},
{
"StartTime": 1352,
"EndTime": 1352,
"X": 256.8456,
"Y": 191.3895
},
{
"StartTime": 1434,
"EndTime": 1434,
"X": 296.873047,
"Y": 191.584747
},
{
"StartTime": 1516,
"EndTime": 1516,
"X": 336.900452,
"Y": 191.78
},
{
"StartTime": 1598,
"EndTime": 1598,
"X": 376.927917,
"Y": 191.975266
},
{
"StartTime": 1680,
"EndTime": 1680,
"X": 337.039948,
"Y": 191.780685
},
{
"StartTime": 1762,
"EndTime": 1762,
"X": 297.0125,
"Y": 191.585434
},
{
"StartTime": 1844,
"EndTime": 1844,
"X": 256.9851,
"Y": 191.390167
},
{
"StartTime": 1926,
"EndTime": 1926,
"X": 216.957672,
"Y": 191.194916
},
{
"StartTime": 2008,
"EndTime": 2008,
"X": 177.069717,
"Y": 191.000336
},
{
"StartTime": 2090,
"EndTime": 2090,
"X": 217.097137,
"Y": 191.1956
},
{
"StartTime": 2172,
"EndTime": 2172,
"X": 257.124573,
"Y": 191.390854
},
{
"StartTime": 2254,
"EndTime": 2254,
"X": 297.152,
"Y": 191.5861
},
{
"StartTime": 2336,
"EndTime": 2336,
"X": 337.179443,
"Y": 191.781372
},
{
"StartTime": 2418,
"EndTime": 2418,
"X": 376.7884,
"Y": 191.974579
},
{
"StartTime": 2500,
"EndTime": 2500,
"X": 336.760956,
"Y": 191.779327
},
{
"StartTime": 2582,
"EndTime": 2582,
"X": 296.733643,
"Y": 191.584076
},
{
"StartTime": 2664,
"EndTime": 2664,
"X": 256.7062,
"Y": 191.388809
},
{
"StartTime": 2746,
"EndTime": 2746,
"X": 216.678772,
"Y": 191.193558
},
{
"StartTime": 2828,
"EndTime": 2828,
"X": 177.348663,
"Y": 191.0017
},
{
"StartTime": 2909,
"EndTime": 2909,
"X": 216.887909,
"Y": 191.19458
},
{
"StartTime": 2991,
"EndTime": 2991,
"X": 256.915344,
"Y": 191.389832
},
{
"StartTime": 3073,
"EndTime": 3073,
"X": 296.942749,
"Y": 191.585083
},
{
"StartTime": 3155,
"EndTime": 3155,
"X": 336.970184,
"Y": 191.78035
},
{
"StartTime": 3201,
"EndTime": 3201,
"X": 376.99762,
"Y": 191.9756
}
]
}]
}

View File

@ -0,0 +1,18 @@
osu file format v14
[General]
StackLeniency: 0.4
Mode: 0
[Difficulty]
CircleSize:4
OverallDifficulty:7
ApproachRate:8
SliderMultiplier:1.6
SliderTickRate:4
[TimingPoints]
369,327.868852459016,4,2,2,32,1,0
[HitObjects]
177,191,369,6,0,L|382:192,7,200

View File

@ -0,0 +1,348 @@
{
"Mappings": [{
"StartTime": 369,
"Objects": [{
"StartTime": 369,
"EndTime": 369,
"X": 127,
"Y": 194
},
{
"StartTime": 450,
"EndTime": 450,
"X": 166.53389,
"Y": 193.8691
},
{
"StartTime": 532,
"EndTime": 532,
"X": 206.555847,
"Y": 193.736572
},
{
"StartTime": 614,
"EndTime": 614,
"X": 246.57782,
"Y": 193.60405
},
{
"StartTime": 696,
"EndTime": 696,
"X": 286.5998,
"Y": 193.471527
},
{
"StartTime": 778,
"EndTime": 778,
"X": 326.621765,
"Y": 193.339
},
{
"StartTime": 860,
"EndTime": 860,
"X": 366.6437,
"Y": 193.206482
},
{
"StartTime": 942,
"EndTime": 942,
"X": 406.66568,
"Y": 193.073959
},
{
"StartTime": 970,
"EndTime": 970,
"X": 420.331726,
"Y": 193.0287
},
{
"StartTime": 997,
"EndTime": 997,
"X": 407.153748,
"Y": 193.072342
},
{
"StartTime": 1079,
"EndTime": 1079,
"X": 367.131775,
"Y": 193.204865
},
{
"StartTime": 1161,
"EndTime": 1161,
"X": 327.1098,
"Y": 193.337387
},
{
"StartTime": 1243,
"EndTime": 1243,
"X": 287.08783,
"Y": 193.46991
},
{
"StartTime": 1325,
"EndTime": 1325,
"X": 247.0659,
"Y": 193.602432
},
{
"StartTime": 1407,
"EndTime": 1407,
"X": 207.043915,
"Y": 193.734955
},
{
"StartTime": 1489,
"EndTime": 1489,
"X": 167.021988,
"Y": 193.867477
},
{
"StartTime": 1571,
"EndTime": 1571,
"X": 127,
"Y": 194
},
{
"StartTime": 1653,
"EndTime": 1653,
"X": 167.021988,
"Y": 193.867477
},
{
"StartTime": 1735,
"EndTime": 1735,
"X": 207.043976,
"Y": 193.734955
},
{
"StartTime": 1817,
"EndTime": 1817,
"X": 247.065887,
"Y": 193.602432
},
{
"StartTime": 1899,
"EndTime": 1899,
"X": 287.08783,
"Y": 193.46991
},
{
"StartTime": 1981,
"EndTime": 1981,
"X": 327.1098,
"Y": 193.337387
},
{
"StartTime": 2062,
"EndTime": 2062,
"X": 366.643738,
"Y": 193.206482
},
{
"StartTime": 2144,
"EndTime": 2144,
"X": 406.665649,
"Y": 193.073959
},
{
"StartTime": 2172,
"EndTime": 2172,
"X": 420.331726,
"Y": 193.0287
},
{
"StartTime": 2199,
"EndTime": 2199,
"X": 407.153748,
"Y": 193.072342
},
{
"StartTime": 2281,
"EndTime": 2281,
"X": 367.1318,
"Y": 193.204865
},
{
"StartTime": 2363,
"EndTime": 2363,
"X": 327.1098,
"Y": 193.337387
},
{
"StartTime": 2445,
"EndTime": 2445,
"X": 287.08783,
"Y": 193.46991
},
{
"StartTime": 2527,
"EndTime": 2527,
"X": 247.065887,
"Y": 193.602432
},
{
"StartTime": 2609,
"EndTime": 2609,
"X": 207.043976,
"Y": 193.734955
},
{
"StartTime": 2691,
"EndTime": 2691,
"X": 167.021988,
"Y": 193.867477
},
{
"StartTime": 2773,
"EndTime": 2773,
"X": 127,
"Y": 194
},
{
"StartTime": 2855,
"EndTime": 2855,
"X": 167.021988,
"Y": 193.867477
},
{
"StartTime": 2937,
"EndTime": 2937,
"X": 207.043976,
"Y": 193.734955
},
{
"StartTime": 3019,
"EndTime": 3019,
"X": 247.065948,
"Y": 193.602432
},
{
"StartTime": 3101,
"EndTime": 3101,
"X": 287.087952,
"Y": 193.46991
},
{
"StartTime": 3183,
"EndTime": 3183,
"X": 327.109772,
"Y": 193.337387
},
{
"StartTime": 3265,
"EndTime": 3265,
"X": 367.131775,
"Y": 193.204865
},
{
"StartTime": 3347,
"EndTime": 3347,
"X": 407.153748,
"Y": 193.072342
},
{
"StartTime": 3374,
"EndTime": 3374,
"X": 420.331726,
"Y": 193.0287
},
{
"StartTime": 3401,
"EndTime": 3401,
"X": 407.153748,
"Y": 193.072342
},
{
"StartTime": 3483,
"EndTime": 3483,
"X": 367.131775,
"Y": 193.204865
},
{
"StartTime": 3565,
"EndTime": 3565,
"X": 327.109772,
"Y": 193.337387
},
{
"StartTime": 3647,
"EndTime": 3647,
"X": 287.087952,
"Y": 193.46991
},
{
"StartTime": 3729,
"EndTime": 3729,
"X": 247.065948,
"Y": 193.602432
},
{
"StartTime": 3811,
"EndTime": 3811,
"X": 207.043976,
"Y": 193.734955
},
{
"StartTime": 3893,
"EndTime": 3893,
"X": 167.021988,
"Y": 193.867477
},
{
"StartTime": 3975,
"EndTime": 3975,
"X": 127,
"Y": 194
},
{
"StartTime": 4057,
"EndTime": 4057,
"X": 167.021988,
"Y": 193.867477
},
{
"StartTime": 4139,
"EndTime": 4139,
"X": 207.043976,
"Y": 193.734955
},
{
"StartTime": 4221,
"EndTime": 4221,
"X": 247.065948,
"Y": 193.602432
},
{
"StartTime": 4303,
"EndTime": 4303,
"X": 287.087952,
"Y": 193.46991
},
{
"StartTime": 4385,
"EndTime": 4385,
"X": 327.109772,
"Y": 193.337387
},
{
"StartTime": 4467,
"EndTime": 4467,
"X": 367.131775,
"Y": 193.204865
},
{
"StartTime": 4540,
"EndTime": 4540,
"X": 420.331726,
"Y": 193.0287
},
{
"StartTime": 4549,
"EndTime": 4549,
"X": 407.153748,
"Y": 193.072342
}
]
}]
}

View File

@ -0,0 +1,19 @@
osu file format v14
[General]
StackLeniency: 0.4
Mode: 0
[Difficulty]
CircleSize:4
OverallDifficulty:7
ApproachRate:8
SliderMultiplier:1.6
SliderTickRate:4
[TimingPoints]
369,327.868852459016,4,2,2,32,1,0
[HitObjects]
// A slider with an un-even amount of ticks
127,194,369,6,0,L|429:193,7,293.333333333333

View File

@ -13,13 +13,15 @@ namespace osu.Game.Rulesets.Osu.UI
protected override Container<Drawable> Content => content;
private readonly Container content;
private const float playfield_size_adjust = 0.8f;
public OsuPlayfieldAdjustmentContainer()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
// Calculated from osu!stable as 512 (default gamefield size) / 640 (default window size)
Size = new Vector2(0.8f);
Size = new Vector2(playfield_size_adjust);
InternalChild = new Container
{
@ -41,7 +43,19 @@ namespace osu.Game.Rulesets.Osu.UI
{
base.Update();
// The following calculation results in a constant of 1.6 when OsuPlayfieldAdjustmentContainer
// is consuming the full game_size. This matches the osu-stable "magic ratio".
//
// game_size = DrawSizePreservingFillContainer.TargetSize = new Vector2(1024, 768)
//
// Parent is a 4:3 aspect enforced, using height as the constricting dimension
// Parent.ChildSize.X = min(game_size.X, game_size.Y * (4 / 3)) * playfield_size_adjust
// Parent.ChildSize.X = 819.2
//
// Scale = 819.2 / 512
// Scale = 1.6
Scale = new Vector2(Parent.ChildSize.X / OsuPlayfield.BASE_SIZE.X);
// Size = 0.625
Size = Vector2.Divide(Vector2.One, Scale);
}
}

View File

@ -20,10 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
[NonParallelizable]
[TestCase("basic")]
[TestCase("slider-generating-drumroll")]
public new void Test(string name)
{
base.Test(name);
}
public void Test(string name) => base.Test(name);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
{

View File

@ -4,7 +4,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.14.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">

View File

@ -12,7 +12,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Difficulty;
@ -107,7 +106,7 @@ namespace osu.Game.Rulesets.Taiko
case ModType.Fun:
return new Mod[]
{
new MultiMod(new ModWindUp<TaikoHitObject>(), new ModWindDown<TaikoHitObject>())
new MultiMod(new ModWindUp(), new ModWindDown())
};
default:

View File

@ -482,5 +482,17 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(hitObjects[0].Samples[0].Bank, hitObjects[0].Samples[1].Bank);
}
}
[Test]
public void TestInvalidEventStillPasses()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var badResStream = TestResources.OpenResource("invalid-events.osu"))
using (var badStream = new StreamReader(badResStream))
{
Assert.DoesNotThrow(() => decoder.Decode(badStream));
}
}
}
}

View File

@ -0,0 +1,116 @@
// 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.Game.Rulesets.Objects;
namespace osu.Game.Tests.Beatmaps
{
[TestFixture]
public class SliderEventGenerationTest
{
private const double start_time = 0;
private const double span_duration = 1000;
[Test]
public void TestSingleSpan()
{
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time));
Assert.That(events[1].Type, Is.EqualTo(SliderEventType.Tick));
Assert.That(events[1].Time, Is.EqualTo(span_duration / 2));
Assert.That(events[3].Type, Is.EqualTo(SliderEventType.Tail));
Assert.That(events[3].Time, Is.EqualTo(span_duration));
}
[Test]
public void TestRepeat()
{
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time));
Assert.That(events[1].Type, Is.EqualTo(SliderEventType.Tick));
Assert.That(events[1].Time, Is.EqualTo(span_duration / 2));
Assert.That(events[2].Type, Is.EqualTo(SliderEventType.Repeat));
Assert.That(events[2].Time, Is.EqualTo(span_duration));
Assert.That(events[3].Type, Is.EqualTo(SliderEventType.Tick));
Assert.That(events[3].Time, Is.EqualTo(span_duration + span_duration / 2));
Assert.That(events[5].Type, Is.EqualTo(SliderEventType.Tail));
Assert.That(events[5].Time, Is.EqualTo(2 * span_duration));
}
[Test]
public void TestNonEvenTicks()
{
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time));
Assert.That(events[1].Type, Is.EqualTo(SliderEventType.Tick));
Assert.That(events[1].Time, Is.EqualTo(300));
Assert.That(events[2].Type, Is.EqualTo(SliderEventType.Tick));
Assert.That(events[2].Time, Is.EqualTo(600));
Assert.That(events[3].Type, Is.EqualTo(SliderEventType.Tick));
Assert.That(events[3].Time, Is.EqualTo(900));
Assert.That(events[4].Type, Is.EqualTo(SliderEventType.Repeat));
Assert.That(events[4].Time, Is.EqualTo(span_duration));
Assert.That(events[5].Type, Is.EqualTo(SliderEventType.Tick));
Assert.That(events[5].Time, Is.EqualTo(1100));
Assert.That(events[6].Type, Is.EqualTo(SliderEventType.Tick));
Assert.That(events[6].Time, Is.EqualTo(1400));
Assert.That(events[7].Type, Is.EqualTo(SliderEventType.Tick));
Assert.That(events[7].Time, Is.EqualTo(1700));
Assert.That(events[9].Type, Is.EqualTo(SliderEventType.Tail));
Assert.That(events[9].Time, Is.EqualTo(2 * span_duration));
}
[Test]
public void TestLegacyLastTickOffset()
{
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray();
Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick));
Assert.That(events[2].Time, Is.EqualTo(900));
}
[Test]
public void TestMinimumTickDistance()
{
const double velocity = 5;
const double min_distance = velocity * 10;
var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray();
Assert.Multiple(() =>
{
int tickIndex = -1;
while (++tickIndex < events.Length)
{
if (events[tickIndex].Type != SliderEventType.Tick)
continue;
Assert.That(events[tickIndex].Time, Is.LessThan(span_duration - min_distance).Or.GreaterThan(span_duration + min_distance));
}
});
}
}
}

View File

@ -0,0 +1,14 @@
osu file format v14
[Events]
bad,event,this,should,fail
//Background and Video events
0,0,"machinetop_background.jpg",0,0
//Break Periods
2,122474,140135
//Storyboard Layer 0 (Background)
this,is,also,bad
//Storyboard Layer 1 (Fail)
//Storyboard Layer 2 (Pass)
//Storyboard Layer 3 (Foreground)
//Storyboard Sound Samples

View File

@ -119,14 +119,14 @@ namespace osu.Game.Tests.Visual.Background
{
performFullSetup();
createFakeStoryboard();
AddStep("Storyboard Enabled", () =>
AddStep("Enable Storyboard", () =>
{
player.ReplacesBackground.Value = true;
player.StoryboardEnabled.Value = true;
});
waitForDim();
AddAssert("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible);
AddStep("Storyboard Disabled", () =>
AddStep("Disable Storyboard", () =>
{
player.ReplacesBackground.Value = false;
player.StoryboardEnabled.Value = false;
@ -149,22 +149,44 @@ namespace osu.Game.Tests.Visual.Background
}
/// <summary>
/// Check if the <see cref="UserDimContainer"/> is properly accepting user-defined visual changes at all.
/// Ensure <see cref="UserDimContainer"/> is properly accepting user-defined visual changes for a background.
/// </summary>
[Test]
public void DisableUserDimTest()
public void DisableUserDimBackgroundTest()
{
performFullSetup();
waitForDim();
AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("EnableUserDim disabled", () => songSelect.DimEnabled.Value = false);
AddStep("Enable user dim", () => songSelect.DimEnabled.Value = false);
waitForDim();
AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsUserBlurDisabled());
AddStep("EnableUserDim enabled", () => songSelect.DimEnabled.Value = true);
AddStep("Disable user dim", () => songSelect.DimEnabled.Value = true);
waitForDim();
AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
}
/// <summary>
/// Ensure <see cref="UserDimContainer"/> is properly accepting user-defined visual changes for a storyboard.
/// </summary>
[Test]
public void DisableUserDimStoryboardTest()
{
performFullSetup();
createFakeStoryboard();
AddStep("Enable Storyboard", () =>
{
player.ReplacesBackground.Value = true;
player.StoryboardEnabled.Value = true;
});
AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true);
AddStep("Set dim level to 1", () => songSelect.DimLevel.Value = 1f);
waitForDim();
AddAssert("Storyboard is invisible", () => !player.IsStoryboardVisible);
AddStep("Disable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = false);
waitForDim();
AddAssert("Storyboard is visible", () => player.IsStoryboardVisible);
}
/// <summary>
/// Check if the visual settings container retains dim and blur when pausing
/// </summary>

View File

@ -1,8 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Timing;
using osu.Game.Beatmaps.Timing;
using osu.Game.Screens.Play;
@ -11,78 +14,172 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestFixture]
public class TestSceneBreakOverlay : OsuTestScene
{
private readonly BreakOverlay breakOverlay;
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(BreakOverlay),
};
private readonly TestBreakOverlay breakOverlay;
private readonly IReadOnlyList<BreakPeriod> testBreaks = new List<BreakPeriod>
{
new BreakPeriod
{
StartTime = 1000,
EndTime = 5000,
},
new BreakPeriod
{
StartTime = 6000,
EndTime = 13500,
},
};
public TestSceneBreakOverlay()
{
Child = breakOverlay = new BreakOverlay(true);
AddStep("2s break", () => startBreak(2000));
AddStep("5s break", () => startBreak(5000));
AddStep("10s break", () => startBreak(10000));
AddStep("15s break", () => startBreak(15000));
AddStep("2s, 2s", startMultipleBreaks);
AddStep("0.5s, 0.7s, 1s, 2s", startAnotherMultipleBreaks);
Add(breakOverlay = new TestBreakOverlay(true));
}
private void startBreak(double duration)
[Test]
public void TestShowBreaks()
{
breakOverlay.Breaks = new List<BreakPeriod>
setClock(false);
addShowBreakStep(2);
addShowBreakStep(5);
addShowBreakStep(15);
}
[Test]
public void TestNoEffectsBreak()
{
var shortBreak = new BreakPeriod { EndTime = 500 };
setClock(true);
loadBreaksStep("short break", new[] { shortBreak });
addBreakSeeks(shortBreak, false);
}
[Test]
public void TestMultipleBreaks()
{
setClock(true);
loadBreaksStep("multiple breaks", testBreaks);
foreach (var b in testBreaks)
addBreakSeeks(b, false);
}
[Test]
public void TestRewindBreaks()
{
setClock(true);
loadBreaksStep("multiple breaks", testBreaks);
foreach (var b in testBreaks.Reverse())
addBreakSeeks(b, true);
}
[Test]
public void TestSkipBreaks()
{
setClock(true);
loadBreaksStep("multiple breaks", testBreaks);
seekAndAssertBreak("seek to break start", testBreaks[1].StartTime, true);
AddAssert("is skipped to break #2", () => breakOverlay.CurrentBreakIndex == 1);
seekAndAssertBreak("seek to break middle", testBreaks[1].StartTime + testBreaks[1].Duration / 2, true);
seekAndAssertBreak("seek to break end", testBreaks[1].EndTime, false);
seekAndAssertBreak("seek to break after end", testBreaks[1].EndTime + 500, false);
}
private void addShowBreakStep(double seconds)
{
AddStep($"show '{seconds}s' break", () => breakOverlay.Breaks = new List<BreakPeriod>
{
new BreakPeriod
{
StartTime = Clock.CurrentTime,
EndTime = Clock.CurrentTime + duration,
EndTime = Clock.CurrentTime + seconds * 1000,
}
};
});
}
private void startMultipleBreaks()
private void setClock(bool useManual)
{
double currentTime = Clock.CurrentTime;
breakOverlay.Breaks = new List<BreakPeriod>
{
new BreakPeriod
{
StartTime = currentTime,
EndTime = currentTime + 2000,
},
new BreakPeriod
{
StartTime = currentTime + 4000,
EndTime = currentTime + 6000,
}
};
AddStep($"set {(useManual ? "manual" : "realtime")} clock", () => breakOverlay.SwitchClock(useManual));
}
private void startAnotherMultipleBreaks()
private void loadBreaksStep(string breakDescription, IReadOnlyList<BreakPeriod> breaks)
{
double currentTime = Clock.CurrentTime;
AddStep($"load {breakDescription}", () => breakOverlay.Breaks = breaks);
seekAndAssertBreak("seek back to 0", 0, false);
}
breakOverlay.Breaks = new List<BreakPeriod>
private void addBreakSeeks(BreakPeriod b, bool isReversed)
{
if (isReversed)
{
new BreakPeriod // Duration is less than 650 - too short to appear
{
StartTime = currentTime,
EndTime = currentTime + 500,
},
new BreakPeriod
{
StartTime = currentTime + 1500,
EndTime = currentTime + 2200,
},
new BreakPeriod
{
StartTime = currentTime + 3200,
EndTime = currentTime + 4200,
},
new BreakPeriod
{
StartTime = currentTime + 5200,
EndTime = currentTime + 7200,
}
};
seekAndAssertBreak("seek to break after end", b.EndTime + 500, false);
seekAndAssertBreak("seek to break end", b.EndTime, false);
seekAndAssertBreak("seek to break middle", b.StartTime + b.Duration / 2, b.HasEffect);
seekAndAssertBreak("seek to break start", b.StartTime, b.HasEffect);
}
else
{
seekAndAssertBreak("seek to break start", b.StartTime, b.HasEffect);
seekAndAssertBreak("seek to break middle", b.StartTime + b.Duration / 2, b.HasEffect);
seekAndAssertBreak("seek to break end", b.EndTime, false);
seekAndAssertBreak("seek to break after end", b.EndTime + 500, false);
}
}
private void seekAndAssertBreak(string seekStepDescription, double time, bool shouldBeBreak)
{
AddStep(seekStepDescription, () => breakOverlay.ManualClockTime = time);
AddAssert($"is{(!shouldBeBreak ? " not" : string.Empty)} break time", () =>
{
breakOverlay.ProgressTime();
return breakOverlay.IsBreakTime.Value == shouldBeBreak;
});
}
private class TestBreakOverlay : BreakOverlay
{
private readonly FramedClock framedManualClock;
private readonly ManualClock manualClock;
private IFrameBasedClock originalClock;
public new int CurrentBreakIndex => base.CurrentBreakIndex;
public double ManualClockTime
{
get => manualClock.CurrentTime;
set => manualClock.CurrentTime = value;
}
public TestBreakOverlay(bool letterboxing)
: base(letterboxing)
{
framedManualClock = new FramedClock(manualClock = new ManualClock());
ProcessCustomClock = false;
}
public void ProgressTime()
{
framedManualClock.ProcessFrame();
Update();
}
public void SwitchClock(bool setManual) => Clock = setManual ? framedManualClock : originalClock;
protected override void LoadComplete()
{
base.LoadComplete();
originalClock = Clock;
}
}
}
}

View File

@ -0,0 +1,36 @@
// 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.Overlays.BeatmapSet;
using System;
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Bindables;
using osu.Game.Screens.Select.Leaderboards;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneLeaderboardScopeSelector : OsuTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(LeaderboardScopeSelector),
};
public TestSceneLeaderboardScopeSelector()
{
Bindable<BeatmapLeaderboardScope> scope = new Bindable<BeatmapLeaderboardScope>();
Add(new LeaderboardScopeSelector
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = { BindTarget = scope }
});
AddStep(@"Select global", () => scope.Value = BeatmapLeaderboardScope.Global);
AddStep(@"Select country", () => scope.Value = BeatmapLeaderboardScope.Country);
AddStep(@"Select friend", () => scope.Value = BeatmapLeaderboardScope.Friend);
}
}
}

View File

@ -9,6 +9,8 @@ using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Users;
using osu.Framework.Bindables;
namespace osu.Game.Tests.Visual.Online
{
@ -23,18 +25,25 @@ namespace osu.Game.Tests.Visual.Online
public TestSceneProfileRulesetSelector()
{
ProfileRulesetSelector selector;
Bindable<User> user = new Bindable<User>();
Child = selector = new ProfileRulesetSelector
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
User = { BindTarget = user }
};
AddStep("set osu! as default", () => selector.SetDefaultRuleset(new OsuRuleset().RulesetInfo));
AddStep("set mania as default", () => selector.SetDefaultRuleset(new ManiaRuleset().RulesetInfo));
AddStep("set taiko as default", () => selector.SetDefaultRuleset(new TaikoRuleset().RulesetInfo));
AddStep("set catch as default", () => selector.SetDefaultRuleset(new CatchRuleset().RulesetInfo));
AddStep("select default ruleset", selector.SelectDefaultRuleset);
AddStep("User with osu as default", () => user.Value = new User { PlayMode = "osu" });
AddStep("User with mania as default", () => user.Value = new User { PlayMode = "mania" });
AddStep("User with taiko as default", () => user.Value = new User { PlayMode = "taiko" });
AddStep("User with catch as default", () => user.Value = new User { PlayMode = "fruits" });
AddStep("null user", () => user.Value = null);
}
}
}

View File

@ -69,28 +69,22 @@ namespace osu.Game.Tests.Visual.Online
}
});
AddStep("null user", () => graph.User.Value = null);
AddStep("null user", () => graph.Statistics.Value = null);
AddStep("rank only", () =>
{
graph.User.Value = new User
graph.Statistics.Value = new UserStatistics
{
Statistics = new UserStatistics
{
Ranks = new UserStatistics.UserRanks { Global = 123456 },
PP = 12345,
}
Ranks = new UserStatistics.UserRanks { Global = 123456 },
PP = 12345,
};
});
AddStep("with rank history", () =>
{
graph.User.Value = new User
graph.Statistics.Value = new UserStatistics
{
Statistics = new UserStatistics
{
Ranks = new UserStatistics.UserRanks { Global = 89000 },
PP = 12345,
},
Ranks = new UserStatistics.UserRanks { Global = 89000 },
PP = 12345,
RankHistory = new User.RankHistoryData
{
Data = data,
@ -100,13 +94,10 @@ namespace osu.Game.Tests.Visual.Online
AddStep("with zero values", () =>
{
graph.User.Value = new User
graph.Statistics.Value = new UserStatistics
{
Statistics = new UserStatistics
{
Ranks = new UserStatistics.UserRanks { Global = 89000 },
PP = 12345,
},
Ranks = new UserStatistics.UserRanks { Global = 89000 },
PP = 12345,
RankHistory = new User.RankHistoryData
{
Data = dataWithZeros,
@ -116,13 +107,10 @@ namespace osu.Game.Tests.Visual.Online
AddStep("small amount of data", () =>
{
graph.User.Value = new User
graph.Statistics.Value = new UserStatistics
{
Statistics = new UserStatistics
{
Ranks = new UserStatistics.UserRanks { Global = 12000 },
PP = 12345,
},
Ranks = new UserStatistics.UserRanks { Global = 12000 },
PP = 12345,
RankHistory = new User.RankHistoryData
{
Data = smallData,
@ -132,13 +120,10 @@ namespace osu.Game.Tests.Visual.Online
AddStep("graph with edges", () =>
{
graph.User.Value = new User
graph.Statistics.Value = new UserStatistics
{
Statistics = new UserStatistics
{
Ranks = new UserStatistics.UserRanks { Global = 12000 },
PP = 12345,
},
Ranks = new UserStatistics.UserRanks { Global = 12000 },
PP = 12345,
RankHistory = new User.RankHistoryData
{
Data = edgyData,

View File

@ -50,12 +50,12 @@ namespace osu.Game.Tests.Visual.Online
{
Current = 727,
Progress = 69,
}
},
RankHistory = new User.RankHistoryData
{
Mode = @"osu",
Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray()
},
RankHistory = new User.RankHistoryData
{
Mode = @"osu",
Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray()
},
},
Badges = new[]
{

View File

@ -0,0 +1,109 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Users;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Taiko;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public class TestSceneUserRequest : OsuTestScene
{
[Resolved]
private IAPIProvider api { get; set; }
private readonly Bindable<User> user = new Bindable<User>();
private GetUserRequest request;
private readonly DimmedLoadingLayer loading;
public TestSceneUserRequest()
{
Add(new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new UserTestContainer
{
User = { BindTarget = user }
},
loading = new DimmedLoadingLayer
{
Alpha = 0
}
}
});
}
protected override void LoadComplete()
{
base.LoadComplete();
AddStep(@"local user", () => getUser());
AddStep(@"local user with taiko ruleset", () => getUser(ruleset: new TaikoRuleset().RulesetInfo));
AddStep(@"cookiezi", () => getUser(124493));
AddStep(@"cookiezi with mania ruleset", () => getUser(124493, new ManiaRuleset().RulesetInfo));
}
private void getUser(long? userId = null, RulesetInfo ruleset = null)
{
loading.Show();
request?.Cancel();
request = new GetUserRequest(userId, ruleset);
request.Success += user =>
{
this.user.Value = user;
loading.Hide();
};
api.Queue(request);
}
private class UserTestContainer : FillFlowContainer
{
public readonly Bindable<User> User = new Bindable<User>();
public UserTestContainer()
{
AutoSizeAxes = Axes.Both;
Direction = FillDirection.Vertical;
}
protected override void LoadComplete()
{
base.LoadComplete();
User.BindValueChanged(onUserUpdate, true);
}
private void onUserUpdate(ValueChangedEvent<User> user)
{
Clear();
AddRange(new Drawable[]
{
new SpriteText
{
Text = $@"Username: {user.NewValue?.Username}"
},
new SpriteText
{
Text = $@"RankedScore: {user.NewValue?.Statistics.RankedScore}"
},
});
}
}
}
}

View File

@ -82,14 +82,13 @@ namespace osu.Game.Tests.Visual.UserInterface
var easierMods = instance.GetModsFor(ModType.DifficultyReduction);
var harderMods = instance.GetModsFor(ModType.DifficultyIncrease);
var assistMods = instance.GetModsFor(ModType.Automation);
var noFailMod = easierMods.FirstOrDefault(m => m is OsuModNoFail);
var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden);
var doubleTimeMod = harderMods.OfType<MultiMod>().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime));
var autoPilotMod = assistMods.FirstOrDefault(m => m is OsuModAutopilot);
var spunOutMod = easierMods.FirstOrDefault(m => m is OsuModSpunOut);
var easy = easierMods.FirstOrDefault(m => m is OsuModEasy);
var hardRock = harderMods.FirstOrDefault(m => m is OsuModHardRock);
@ -101,7 +100,7 @@ namespace osu.Game.Tests.Visual.UserInterface
testMultiplierTextColour(noFailMod, modSelect.LowMultiplierColour);
testMultiplierTextColour(hiddenMod, modSelect.HighMultiplierColour);
testUnimplementedMod(autoPilotMod);
testUnimplementedMod(spunOutMod);
}
[Test]

View File

@ -7,6 +7,7 @@ using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Graphics;
using osu.Game.Overlays;
using osu.Game.Overlays.OSD;
namespace osu.Game.Tests.Visual.UserInterface
{
@ -22,6 +23,12 @@ namespace osu.Game.Tests.Visual.UserInterface
osd.BeginTracking(this, config);
Add(osd);
AddStep("Display empty osd toast", () => osd.Display(new EmptyToast()));
AddAssert("Toast width is 240", () => osd.Child.Width == 240);
AddStep("Display toast with lengthy text", () => osd.Display(new LengthyToast()));
AddAssert("Toast width is greater than 240", () => osd.Child.Width > 240);
AddRepeatStep("Change toggle (no bind)", () => config.ToggleSetting(TestConfigSetting.ToggleSettingNoKeybind), 2);
AddRepeatStep("Change toggle (with bind)", () => config.ToggleSetting(TestConfigSetting.ToggleSettingWithKeybind), 2);
AddRepeatStep("Change enum (no bind)", () => config.IncrementEnumSetting(TestConfigSetting.EnumSettingNoKeybind), 3);
@ -86,6 +93,22 @@ namespace osu.Game.Tests.Visual.UserInterface
Setting4
}
private class EmptyToast : Toast
{
public EmptyToast()
: base("", "", "")
{
}
}
private class LengthyToast : Toast
{
public LengthyToast()
: base("Toast with a very very very long text", "A very very very very very very long text also", "A very very very very very long shortcut")
{
}
}
private class TestOnScreenDisplay : OnScreenDisplay
{
protected override void DisplayTemporarily(Drawable toDisplay) => toDisplay.FadeIn().ResizeHeightTo(110);

View File

@ -5,7 +5,7 @@
<PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.14.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">

View File

@ -7,7 +7,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.14.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -88,7 +88,7 @@ namespace osu.Game.Tournament.Screens.Ladder
};
}
private Cached layout = new Cached();
private readonly Cached layout = new Cached();
protected override void Update()
{

View File

@ -60,5 +60,7 @@ namespace osu.Game.Beatmaps
public class Beatmap : Beatmap<HitObject>
{
public new Beatmap Clone() => (Beatmap)base.Clone();
public override string ToString() => BeatmapInfo?.ToString() ?? base.ToString();
}
}

View File

@ -5,7 +5,6 @@ using System;
using System.IO;
using System.Linq;
using osu.Framework.IO.File;
using osu.Framework.Logging;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Beatmaps.ControlPoints;
@ -290,8 +289,9 @@ namespace osu.Game.Beatmaps.Formats
string[] split = line.Split(',');
EventType type;
if (!Enum.TryParse(split[0], out type))
throw new InvalidDataException($@"Unknown event type {split[0]}");
throw new InvalidDataException($@"Unknown event type: {split[0]}");
switch (type)
{
@ -319,90 +319,79 @@ namespace osu.Game.Beatmaps.Formats
private void handleTimingPoint(string line)
{
try
string[] split = line.Split(',');
double time = getOffsetTime(Parsing.ParseDouble(split[0].Trim()));
double beatLength = Parsing.ParseDouble(split[1].Trim());
double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1;
TimeSignatures timeSignature = TimeSignatures.SimpleQuadruple;
if (split.Length >= 3)
timeSignature = split[2][0] == '0' ? TimeSignatures.SimpleQuadruple : (TimeSignatures)Parsing.ParseInt(split[2]);
LegacySampleBank sampleSet = defaultSampleBank;
if (split.Length >= 4)
sampleSet = (LegacySampleBank)Parsing.ParseInt(split[3]);
int customSampleBank = 0;
if (split.Length >= 5)
customSampleBank = Parsing.ParseInt(split[4]);
int sampleVolume = defaultSampleVolume;
if (split.Length >= 6)
sampleVolume = Parsing.ParseInt(split[5]);
bool timingChange = true;
if (split.Length >= 7)
timingChange = split[6][0] == '1';
bool kiaiMode = false;
bool omitFirstBarSignature = false;
if (split.Length >= 8)
{
string[] split = line.Split(',');
double time = getOffsetTime(Parsing.ParseDouble(split[0].Trim()));
double beatLength = Parsing.ParseDouble(split[1].Trim());
double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1;
TimeSignatures timeSignature = TimeSignatures.SimpleQuadruple;
if (split.Length >= 3)
timeSignature = split[2][0] == '0' ? TimeSignatures.SimpleQuadruple : (TimeSignatures)Parsing.ParseInt(split[2]);
LegacySampleBank sampleSet = defaultSampleBank;
if (split.Length >= 4)
sampleSet = (LegacySampleBank)Parsing.ParseInt(split[3]);
int customSampleBank = 0;
if (split.Length >= 5)
customSampleBank = Parsing.ParseInt(split[4]);
int sampleVolume = defaultSampleVolume;
if (split.Length >= 6)
sampleVolume = Parsing.ParseInt(split[5]);
bool timingChange = true;
if (split.Length >= 7)
timingChange = split[6][0] == '1';
bool kiaiMode = false;
bool omitFirstBarSignature = false;
if (split.Length >= 8)
{
EffectFlags effectFlags = (EffectFlags)Parsing.ParseInt(split[7]);
kiaiMode = effectFlags.HasFlag(EffectFlags.Kiai);
omitFirstBarSignature = effectFlags.HasFlag(EffectFlags.OmitFirstBarLine);
}
string stringSampleSet = sampleSet.ToString().ToLowerInvariant();
if (stringSampleSet == @"none")
stringSampleSet = @"normal";
if (timingChange)
{
var controlPoint = CreateTimingControlPoint();
controlPoint.Time = time;
controlPoint.BeatLength = beatLength;
controlPoint.TimeSignature = timeSignature;
handleTimingControlPoint(controlPoint);
}
handleDifficultyControlPoint(new DifficultyControlPoint
{
Time = time,
SpeedMultiplier = speedMultiplier,
AutoGenerated = timingChange
});
handleEffectControlPoint(new EffectControlPoint
{
Time = time,
KiaiMode = kiaiMode,
OmitFirstBarLine = omitFirstBarSignature,
AutoGenerated = timingChange
});
handleSampleControlPoint(new LegacySampleControlPoint
{
Time = time,
SampleBank = stringSampleSet,
SampleVolume = sampleVolume,
CustomSampleBank = customSampleBank,
AutoGenerated = timingChange
});
EffectFlags effectFlags = (EffectFlags)Parsing.ParseInt(split[7]);
kiaiMode = effectFlags.HasFlag(EffectFlags.Kiai);
omitFirstBarSignature = effectFlags.HasFlag(EffectFlags.OmitFirstBarLine);
}
catch (FormatException)
string stringSampleSet = sampleSet.ToString().ToLowerInvariant();
if (stringSampleSet == @"none")
stringSampleSet = @"normal";
if (timingChange)
{
Logger.Log("A timing point could not be parsed correctly and will be ignored", LoggingTarget.Runtime, LogLevel.Important);
var controlPoint = CreateTimingControlPoint();
controlPoint.Time = time;
controlPoint.BeatLength = beatLength;
controlPoint.TimeSignature = timeSignature;
handleTimingControlPoint(controlPoint);
}
catch (OverflowException)
handleDifficultyControlPoint(new DifficultyControlPoint
{
Logger.Log("A timing point could not be parsed correctly and will be ignored", LoggingTarget.Runtime, LogLevel.Important);
}
Time = time,
SpeedMultiplier = speedMultiplier,
AutoGenerated = timingChange
});
handleEffectControlPoint(new EffectControlPoint
{
Time = time,
KiaiMode = kiaiMode,
OmitFirstBarLine = omitFirstBarSignature,
AutoGenerated = timingChange
});
handleSampleControlPoint(new LegacySampleControlPoint
{
Time = time,
SampleBank = stringSampleSet,
SampleVolume = sampleVolume,
CustomSampleBank = customSampleBank,
AutoGenerated = timingChange
});
}
private void handleTimingControlPoint(TimingControlPoint newPoint)

View File

@ -36,7 +36,7 @@ namespace osu.Game.Beatmaps.Formats
{
if (!Enum.TryParse(line.Substring(1, line.Length - 2), out section))
{
Logger.Log($"Unknown section \"{line}\" in {output}");
Logger.Log($"Unknown section \"{line}\" in \"{output}\"");
section = Section.None;
}
@ -49,7 +49,7 @@ namespace osu.Game.Beatmaps.Formats
}
catch (Exception e)
{
Logger.Error(e, $"Failed to process line \"{line}\" into {output}");
Logger.Log($"Failed to process line \"{line}\" into \"{output}\": {e.Message}", LoggingTarget.Runtime, LogLevel.Important);
}
}
}

View File

@ -82,8 +82,9 @@ namespace osu.Game.Beatmaps.Formats
storyboardSprite = null;
EventType type;
if (!Enum.TryParse(split[0], out type))
throw new InvalidDataException($@"Unknown event type {split[0]}");
throw new InvalidDataException($@"Unknown event type: {split[0]}");
switch (type)
{

View File

@ -9,7 +9,7 @@ namespace osu.Game.Beatmaps.Legacy
public enum LegacyMods
{
None = 0,
NoFail = 1 << 0,
NoFail = 1,
Easy = 1 << 1,
TouchDevice = 1 << 2,
Hidden = 1 << 3,

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Screens.Play;
namespace osu.Game.Beatmaps.Timing
{
public class BreakPeriod
@ -35,6 +37,6 @@ namespace osu.Game.Beatmaps.Timing
/// </summary>
/// <param name="time">The time to check in milliseconds.</param>
/// <returns>Whether the time falls within this <see cref="BreakPeriod"/>.</returns>
public bool Contains(double time) => time >= StartTime && time <= EndTime;
public bool Contains(double time) => time >= StartTime && time <= EndTime - BreakOverlay.BREAK_FADE_DURATION;
}
}

View File

@ -89,6 +89,14 @@ namespace osu.Game.Beatmaps
return path;
}
/// <summary>
/// Creates a <see cref="IBeatmapConverter"/> to convert a <see cref="IBeatmap"/> for a specified <see cref="Ruleset"/>.
/// </summary>
/// <param name="beatmap">The <see cref="IBeatmap"/> to be converted.</param>
/// <param name="ruleset">The <see cref="Ruleset"/> for which <paramref name="beatmap"/> should be converted.</param>
/// <returns>The applicable <see cref="IBeatmapConverter"/>.</returns>
protected virtual IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) => ruleset.CreateBeatmapConverter(beatmap);
/// <summary>
/// Constructs a playable <see cref="IBeatmap"/> from <see cref="Beatmap"/> using the applicable converters for a specific <see cref="RulesetInfo"/>.
/// <para>
@ -104,7 +112,7 @@ namespace osu.Game.Beatmaps
{
var rulesetInstance = ruleset.CreateInstance();
IBeatmapConverter converter = rulesetInstance.CreateBeatmapConverter(Beatmap);
IBeatmapConverter converter = CreateBeatmapConverter(Beatmap, rulesetInstance);
// Check if the beatmap can be converted
if (!converter.CanConvert)
@ -141,6 +149,9 @@ namespace osu.Game.Beatmaps
processor?.PostProcess();
foreach (var mod in mods.OfType<IApplicableToBeatmap>())
mod.ApplyToBeatmap(converted);
return converted;
}

View File

@ -31,10 +31,21 @@ namespace osu.Game.Database
/// </summary>
/// <typeparam name="TModel">The model type.</typeparam>
/// <typeparam name="TFileModel">The associated file join type.</typeparam>
public abstract class ArchiveModelManager<TModel, TFileModel> : ArchiveModelManager, ICanAcceptFiles, IModelManager<TModel>
public abstract class ArchiveModelManager<TModel, TFileModel> : ICanAcceptFiles, IModelManager<TModel>
where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
where TFileModel : INamedFileInfo, new()
{
private const int import_queue_request_concurrency = 1;
/// <summary>
/// A singleton scheduler shared by all <see cref="ArchiveModelManager{TModel,TFileModel}"/>.
/// </summary>
/// <remarks>
/// This scheduler generally performs IO and CPU intensive work so concurrency is limited harshly.
/// It is mainly being used as a queue mechanism for large imports.
/// </remarks>
private static readonly ThreadedTaskScheduler import_scheduler = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelManager<TModel, TFileModel>));
/// <summary>
/// Set an endpoint for notifications to be posted to.
/// </summary>
@ -336,7 +347,7 @@ namespace osu.Game.Database
flushEvents(true);
return item;
}, cancellationToken, TaskCreationOptions.HideScheduler, IMPORT_SCHEDULER).Unwrap();
}, cancellationToken, TaskCreationOptions.HideScheduler, import_scheduler).Unwrap();
/// <summary>
/// Perform an update of the specified item.
@ -646,18 +657,4 @@ namespace osu.Game.Database
#endregion
}
public abstract class ArchiveModelManager
{
private const int import_queue_request_concurrency = 1;
/// <summary>
/// A singleton scheduler shared by all <see cref="ArchiveModelManager{TModel,TFileModel}"/>.
/// </summary>
/// <remarks>
/// This scheduler generally performs IO and CPU intensive work so concurrency is limited harshly.
/// It is mainly being used as a queue mechanism for large imports.
/// </remarks>
protected static readonly ThreadedTaskScheduler IMPORT_SCHEDULER = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelManager));
}
}

View File

@ -191,7 +191,7 @@ namespace osu.Game.Graphics.Backgrounds
private readonly List<TriangleParticle> parts = new List<TriangleParticle>();
private Vector2 size;
private TriangleBatch<TexturedVertex2D> vertexBatch;
private QuadBatch<TexturedVertex2D> vertexBatch;
public TrianglesDrawNode(Triangles source)
: base(source)
@ -217,7 +217,7 @@ namespace osu.Game.Graphics.Backgrounds
if (Source.AimCount > 0 && (vertexBatch == null || vertexBatch.Size != Source.AimCount))
{
vertexBatch?.Dispose();
vertexBatch = new TriangleBatch<TexturedVertex2D>(Source.AimCount, 1);
vertexBatch = new QuadBatch<TexturedVertex2D>(Source.AimCount, 1);
}
shader.Bind();

View File

@ -6,7 +6,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osuTK.Graphics;
namespace osu.Game.Graphics.Containers
{
@ -36,6 +35,8 @@ namespace osu.Game.Graphics.Containers
protected Bindable<bool> ShowStoryboard { get; private set; }
protected double DimLevel => EnableUserDim.Value ? UserDimLevel.Value : 0;
protected override Container<Drawable> Content => dimContent;
private Container dimContent { get; }
@ -78,8 +79,8 @@ namespace osu.Game.Graphics.Containers
{
ContentDisplayed = ShowDimContent;
dimContent.FadeTo((ContentDisplayed) ? 1 : 0, BACKGROUND_FADE_DURATION, Easing.OutQuint);
dimContent.FadeColour(EnableUserDim.Value ? OsuColour.Gray(1 - (float)UserDimLevel.Value) : Color4.White, BACKGROUND_FADE_DURATION, Easing.OutQuint);
dimContent.FadeTo(ContentDisplayed ? 1 : 0, BACKGROUND_FADE_DURATION, Easing.OutQuint);
dimContent.FadeColour(OsuColour.Gray(1 - (float)DimLevel), BACKGROUND_FADE_DURATION, Easing.OutQuint);
}
}
}

View File

@ -92,7 +92,8 @@ namespace osu.Game.Graphics
using (var image = await host.TakeScreenshotAsync())
{
Interlocked.Decrement(ref screenShotTasks);
if (Interlocked.Decrement(ref screenShotTasks) == 0 && cursorVisibility.Value == false)
cursorVisibility.Value = true;
var fileName = getFileName();
if (fileName == null) return;
@ -125,14 +126,6 @@ namespace osu.Game.Graphics
}
});
protected override void Update()
{
base.Update();
if (cursorVisibility.Value == false && Interlocked.CompareExchange(ref screenShotTasks, 0, 0) == 0)
cursorVisibility.Value = true;
}
private string getFileName()
{
var dt = DateTime.Now;

View File

@ -24,8 +24,8 @@ namespace osu.Game.Graphics.UserInterface
Child = button = new TwoLayerButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Text = @"back",
Icon = OsuIcon.LeftCircle,
Action = () => Action?.Invoke()

View File

@ -110,7 +110,7 @@ namespace osu.Game.Graphics.UserInterface
[Flags]
public enum BarDirection
{
LeftToRight = 1 << 0,
LeftToRight = 1,
RightToLeft = 1 << 1,
TopToBottom = 1 << 2,
BottomToTop = 1 << 3,

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osuTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Extensions.Color4Extensions;
namespace osu.Game.Graphics.UserInterface
{
public class DimmedLoadingLayer : VisibilityContainer
{
private const float transition_duration = 250;
private readonly LoadingAnimation loading;
public DimmedLoadingLayer()
{
RelativeSizeAxes = Axes.Both;
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(0.5f),
},
loading = new LoadingAnimation(),
};
}
protected override void PopIn()
{
this.FadeIn(transition_duration, Easing.OutQuint);
loading.Show();
}
protected override void PopOut()
{
this.FadeOut(transition_duration, Easing.OutQuint);
loading.Hide();
}
}
}

View File

@ -93,7 +93,7 @@ namespace osu.Game.Graphics.UserInterface
return base.Invalidate(invalidation, source, shallPropagate);
}
private Cached pathCached = new Cached();
private readonly Cached pathCached = new Cached();
protected override void Update()
{

View File

@ -31,6 +31,11 @@ namespace osu.Game.Graphics.UserInterface
protected virtual float StripWidth() => TabContainer.Children.Sum(c => c.IsPresent ? c.DrawWidth + TabContainer.Spacing.X : 0) - TabContainer.Spacing.X;
protected virtual float StripHeight() => 1;
/// <summary>
/// Whether entries should be automatically populated if <see cref="T"/> is an <see cref="Enum"/> type.
/// </summary>
protected virtual bool AddEnumEntriesAutomatically => true;
private static bool isEnumType => typeof(T).IsEnum;
public OsuTabControl()
@ -45,7 +50,7 @@ namespace osu.Game.Graphics.UserInterface
Colour = Color4.White.Opacity(0),
});
if (isEnumType)
if (isEnumType && AddEnumEntriesAutomatically)
foreach (var val in (T[])Enum.GetValues(typeof(T)))
AddItem(val);
}

View File

@ -81,7 +81,8 @@ namespace osu.Game.Graphics.UserInterface
Colour = Color4.White,
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
}
},
new HoverClickSounds()
};
Current.ValueChanged += selected =>

View File

@ -24,7 +24,13 @@ namespace osu.Game.Graphics.UserInterface
Height = 30;
}
public class PageTabItem : TabItem<T>
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AccentColour = colours.Yellow;
}
public class PageTabItem : TabItem<T>, IHasAccentColour
{
private const float transition_duration = 100;
@ -32,6 +38,18 @@ namespace osu.Game.Graphics.UserInterface
protected readonly SpriteText Text;
private Color4 accentColour;
public Color4 AccentColour
{
get => accentColour;
set
{
accentColour = value;
box.Colour = accentColour;
}
}
public PageTabItem(T value)
: base(value)
{
@ -63,12 +81,6 @@ namespace osu.Game.Graphics.UserInterface
Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Exo, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
box.Colour = colours.Yellow;
}
protected override bool OnHover(HoverEvent e)
{
if (!Active.Value)

View File

@ -15,7 +15,7 @@ namespace osu.Game.Graphics.UserInterface
{
public const float ICON_WIDTH = ICON_SIZE + icon_spacing;
protected const float ICON_SIZE = 25;
public const float ICON_SIZE = 25;
private SpriteIcon iconSprite;
private readonly OsuSpriteText titleText, pageText;

View File

@ -0,0 +1,64 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osuTK;
namespace osu.Game.Graphics.UserInterface
{
/// <summary>
/// A custom icon class for use with <see cref="ScreenTitle.CreateIcon()"/> based off a texture resource.
/// </summary>
public class ScreenTitleTextureIcon : CompositeDrawable
{
private const float circle_allowance = 0.8f;
private readonly string textureName;
public ScreenTitleTextureIcon(string textureName)
{
this.textureName = textureName;
}
[BackgroundDependencyLoader]
private void load(TextureStore textures, OsuColour colours)
{
Size = new Vector2(ScreenTitle.ICON_SIZE / circle_allowance);
InternalChildren = new Drawable[]
{
new CircularContainer
{
Masking = true,
BorderColour = colours.Violet,
BorderThickness = 3,
MaskingSmoothness = 1,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Sprite
{
RelativeSizeAxes = Axes.Both,
Texture = textures.Get(textureName),
Size = new Vector2(circle_allowance),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Violet,
Alpha = 0,
AlwaysPresent = true,
},
}
},
};
}
}
}

View File

@ -15,6 +15,7 @@ using System;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Screens.Select;
namespace osu.Game.Graphics.UserInterface
{
@ -28,7 +29,9 @@ namespace osu.Game.Graphics.UserInterface
private const int transform_time = 600;
private const int pulse_length = 250;
private const float shear = 0.1f;
private const float shear_width = 5f;
private static readonly Vector2 shear = new Vector2(shear_width / Footer.HEIGHT, 0);
public static readonly Vector2 SIZE_EXTENDED = new Vector2(140, 50);
public static readonly Vector2 SIZE_RETRACTED = new Vector2(100, 50);
@ -56,7 +59,7 @@ namespace osu.Game.Graphics.UserInterface
c1.Origin = c1.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopLeft : Anchor.TopRight;
c2.Origin = c2.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopRight : Anchor.TopLeft;
X = value.HasFlag(Anchor.x2) ? SIZE_RETRACTED.X * shear * 0.5f : 0;
X = value.HasFlag(Anchor.x2) ? SIZE_RETRACTED.X * shear.X * 0.5f : 0;
Remove(c1);
Remove(c2);
@ -70,6 +73,7 @@ namespace osu.Game.Graphics.UserInterface
public TwoLayerButton()
{
Size = SIZE_RETRACTED;
Shear = shear;
Children = new Drawable[]
{
@ -82,7 +86,6 @@ namespace osu.Game.Graphics.UserInterface
new Container
{
RelativeSizeAxes = Axes.Both,
Shear = new Vector2(shear, 0),
Masking = true,
MaskingSmoothness = 2,
EdgeEffect = new EdgeEffectParameters
@ -105,6 +108,7 @@ namespace osu.Game.Graphics.UserInterface
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = -shear,
},
}
},
@ -119,7 +123,6 @@ namespace osu.Game.Graphics.UserInterface
new Container
{
RelativeSizeAxes = Axes.Both,
Shear = new Vector2(shear, 0),
Masking = true,
MaskingSmoothness = 2,
EdgeEffect = new EdgeEffectParameters
@ -144,6 +147,7 @@ namespace osu.Game.Graphics.UserInterface
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Shear = -shear,
}
}
},
@ -188,7 +192,6 @@ namespace osu.Game.Graphics.UserInterface
var flash = new Box
{
RelativeSizeAxes = Axes.Both,
Shear = new Vector2(shear, 0),
Colour = Color4.White.Opacity(0.5f),
};
Add(flash);

View File

@ -27,6 +27,7 @@ namespace osu.Game.Online.API.Requests
{
Favourite,
RankedAndApproved,
Loved,
Unranked,
Graveyard
}

View File

@ -2,18 +2,21 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Users;
using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests
{
public class GetUserRequest : APIRequest<User>
{
private readonly long? userId;
private readonly RulesetInfo ruleset;
public GetUserRequest(long? userId = null)
public GetUserRequest(long? userId = null, RulesetInfo ruleset = null)
{
this.userId = userId;
this.ruleset = ruleset;
}
protected override string Target => userId.HasValue ? $@"users/{userId}" : @"me";
protected override string Target => userId.HasValue ? $@"users/{userId}/{ruleset?.ShortName}" : $@"me/{ruleset?.ShortName}";
}
}

View File

@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.IO.Network;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests
{
@ -10,12 +12,24 @@ namespace osu.Game.Online.API.Requests
{
private readonly long userId;
private readonly ScoreType type;
private readonly RulesetInfo ruleset;
public GetUserScoresRequest(long userId, ScoreType type, int page = 0, int itemsPerPage = 5)
public GetUserScoresRequest(long userId, ScoreType type, int page = 0, int itemsPerPage = 5, RulesetInfo ruleset = null)
: base(page, itemsPerPage)
{
this.userId = userId;
this.type = type;
this.ruleset = ruleset;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
if (ruleset != null)
req.AddParameter("mode", ruleset.ShortName);
return req;
}
protected override string Target => $@"users/{userId}/scores/{type.ToString().ToLowerInvariant()}";

View File

@ -27,24 +27,25 @@ namespace osu.Game.Online.API.Requests
}
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
protected override string Target => $@"beatmapsets/search?q={query}&m={ruleset.ID ?? 0}&s={(int)searchCategory}&sort={sortCriteria.ToString().ToLowerInvariant()}_{directionString}";
protected override string Target => $@"beatmapsets/search?q={query}&m={ruleset.ID ?? 0}&s={searchCategory.ToString().ToLowerInvariant()}&sort={sortCriteria.ToString().ToLowerInvariant()}_{directionString}";
}
public enum BeatmapSearchCategory
{
Any = 7,
Any,
[Description("Ranked & Approved")]
RankedApproved = 0,
Qualified = 3,
Loved = 8,
Favourites = 2,
[Description("Has Leaderboard")]
Leaderboard,
Ranked,
Qualified,
Loved,
Favourites,
[Description("Pending & WIP")]
PendingWIP = 4,
Graveyard = 5,
Pending,
Graveyard,
[Description("My Maps")]
MyMaps = 6,
Mine,
}
}

View File

@ -213,8 +213,27 @@ namespace osu.Game.Online.Chat
PostMessage(content, true);
break;
case "join":
if (string.IsNullOrWhiteSpace(content))
{
target.AddNewMessages(new ErrorMessage("Usage: /join [channel]"));
break;
}
var channel = availableChannels.Where(c => c.Name == content || c.Name == $"#{content}").FirstOrDefault();
if (channel == null)
{
target.AddNewMessages(new ErrorMessage($"Channel '{content}' not found."));
break;
}
JoinChannel(channel);
CurrentChannel.Value = channel;
break;
case "help":
target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action]"));
target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel]"));
break;
default:

View File

@ -8,7 +8,7 @@ namespace osu.Game.Online.Chat
public ErrorMessage(string message)
: base(message)
{
Sender.Colour = @"ff0000";
// todo: this should likely be styled differently in the future.
}
}
}

View File

@ -48,22 +48,24 @@ namespace osu.Game.Online
attachDownload(manager.GetExistingDownload(modelInfo.NewValue));
}, true);
manager.DownloadBegan += download =>
{
if (download.Model.Equals(Model.Value))
attachDownload(download);
};
manager.DownloadFailed += download =>
{
if (download.Model.Equals(Model.Value))
attachDownload(null);
};
manager.DownloadBegan += downloadBegan;
manager.DownloadFailed += downloadFailed;
manager.ItemAdded += itemAdded;
manager.ItemRemoved += itemRemoved;
}
private void downloadBegan(ArchiveDownloadRequest<TModel> request)
{
if (request.Model.Equals(Model.Value))
attachDownload(request);
}
private void downloadFailed(ArchiveDownloadRequest<TModel> request)
{
if (request.Model.Equals(Model.Value))
attachDownload(null);
}
private ArchiveDownloadRequest<TModel> attachedRequest;
private void attachDownload(ArchiveDownloadRequest<TModel> request)
@ -126,8 +128,10 @@ namespace osu.Game.Online
if (manager != null)
{
manager.DownloadBegan -= attachDownload;
manager.DownloadBegan -= downloadBegan;
manager.DownloadFailed -= downloadFailed;
manager.ItemAdded -= itemAdded;
manager.ItemRemoved -= itemRemoved;
}
State.UnbindAll();

View File

@ -311,7 +311,9 @@ namespace osu.Game
if (nextBeatmap?.Track != null)
nextBeatmap.Track.Completed += currentTrackCompleted;
beatmap.OldValue?.Dispose();
using (var oldBeatmap = beatmap.OldValue)
if (oldBeatmap?.Track != null)
oldBeatmap.Track.Completed -= currentTrackCompleted;
nextBeatmap?.LoadBeatmapAsync();
}

View File

@ -0,0 +1,119 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.UserInterface;
using osu.Game.Screens.Select.Leaderboards;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osuTK;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Framework.Allocation;
using osuTK.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Input.Events;
namespace osu.Game.Overlays.BeatmapSet
{
public class LeaderboardScopeSelector : PageTabControl<BeatmapLeaderboardScope>
{
protected override bool AddEnumEntriesAutomatically => false;
protected override Dropdown<BeatmapLeaderboardScope> CreateDropdown() => null;
protected override TabItem<BeatmapLeaderboardScope> CreateTabItem(BeatmapLeaderboardScope value) => new ScopeSelectorTabItem(value);
public LeaderboardScopeSelector()
{
RelativeSizeAxes = Axes.X;
AddItem(BeatmapLeaderboardScope.Global);
AddItem(BeatmapLeaderboardScope.Country);
AddItem(BeatmapLeaderboardScope.Friend);
AddInternal(new GradientLine
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
});
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AccentColour = colours.Blue;
}
protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(20, 0),
};
private class ScopeSelectorTabItem : PageTabItem
{
public ScopeSelectorTabItem(BeatmapLeaderboardScope value)
: base(value)
{
Text.Font = OsuFont.GetFont(size: 16);
}
protected override bool OnHover(HoverEvent e)
{
Text.FadeColour(AccentColour);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
Text.FadeColour(Color4.White);
}
}
private class GradientLine : GridContainer
{
public GradientLine()
{
RelativeSizeAxes = Axes.X;
Size = new Vector2(0.8f, 1.5f);
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(mode: GridSizeMode.Relative, size: 0.4f),
new Dimension(),
};
Content = new[]
{
new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(Color4.Transparent, Color4.Gray),
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Gray,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(Color4.Gray, Color4.Transparent),
},
}
};
}
}
}
}

View File

@ -132,7 +132,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
Scores = null;
if (beatmap?.OnlineBeatmapID.HasValue != true)
if (beatmap?.OnlineBeatmapID.HasValue != true || beatmap.Status <= BeatmapSetOnlineStatus.Pending)
return;
loadingAnimation.Show();

View File

@ -7,13 +7,11 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osuTK;
namespace osu.Game.Overlays.Changelog
{
@ -123,48 +121,7 @@ namespace osu.Game.Overlays.Changelog
AccentColour = colours.Violet;
}
protected override Drawable CreateIcon() => new ChangelogIcon();
internal class ChangelogIcon : CompositeDrawable
{
private const float circle_allowance = 0.8f;
[BackgroundDependencyLoader]
private void load(TextureStore textures, OsuColour colours)
{
Size = new Vector2(ICON_SIZE / circle_allowance);
InternalChildren = new Drawable[]
{
new CircularContainer
{
Masking = true,
BorderColour = colours.Violet,
BorderThickness = 3,
MaskingSmoothness = 1,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Sprite
{
RelativeSizeAxes = Axes.Both,
Texture = textures.Get(@"Icons/changelog"),
Size = new Vector2(circle_allowance),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Violet,
Alpha = 0,
AlwaysPresent = true,
},
}
},
};
}
}
protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog");
}
}
}

View File

@ -256,6 +256,9 @@ namespace osu.Game.Overlays
loadedChannels.Add(loaded);
LoadComponentAsync(loaded, l =>
{
if (currentChannel.Value != e.NewValue)
return;
loading.Hide();
currentChannelContainer.Clear(false);
@ -381,7 +384,18 @@ namespace osu.Game.Overlays
foreach (Channel channel in channels)
{
ChannelTabControl.RemoveChannel(channel);
loadedChannels.Remove(loadedChannels.Find(c => c.Channel == channel));
var loaded = loadedChannels.Find(c => c.Channel == channel);
if (loaded != null)
{
loadedChannels.Remove(loaded);
// Because the container is only cleared in the async load callback of a new channel, it is forcefully cleared
// to ensure that the previous channel doesn't get updated after it's disposed
currentChannelContainer.Remove(loaded);
loaded.Dispose();
}
}
}

View File

@ -18,6 +18,7 @@ namespace osu.Game.Overlays.Direct
protected override Color4 BackgroundColour => OsuColour.FromHex(@"384552");
protected override DirectSortCriteria DefaultTab => DirectSortCriteria.Ranked;
protected override BeatmapSearchCategory DefaultCategory => BeatmapSearchCategory.Leaderboard;
protected override Drawable CreateSupplementaryControls() => rulesetSelector = new DirectRulesetSelector();

View File

@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Mods
}
}
foregroundIcon.Highlighted = Selected;
foregroundIcon.Highlighted.Value = Selected;
SelectionChanged?.Invoke(SelectedMod);
return true;

View File

@ -0,0 +1,84 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.OSD
{
public abstract class Toast : Container
{
private const int toast_minimum_width = 240;
private readonly Container content;
protected override Container<Drawable> Content => content;
protected readonly OsuSpriteText ValueText;
protected Toast(string description, string value, string shortcut)
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
// A toast's height is decided (and transformed) by the containing OnScreenDisplay.
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
InternalChildren = new Drawable[]
{
new Container //this container exists just to set a minimum width for the toast
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = toast_minimum_width
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.7f
},
content = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Padding = new MarginPadding(10),
Name = "Description",
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Black),
Spacing = new Vector2(1, 0),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = description.ToUpperInvariant()
},
ValueText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light),
Padding = new MarginPadding { Left = 10, Right = 10 },
Name = "Value",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = value
},
new OsuSpriteText
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Name = "Shortcut",
Alpha = 0.3f,
Margin = new MarginPadding { Bottom = 15 },
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
Text = string.IsNullOrEmpty(shortcut) ? "NO KEY BOUND" : shortcut.ToUpperInvariant()
},
};
}
}
}

View File

@ -0,0 +1,147 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.OSD
{
public class TrackedSettingToast : Toast
{
private const int lights_bottom_margin = 40;
public TrackedSettingToast(SettingDescription description)
: base(description.Name, description.Value, description.Shortcut)
{
FillFlowContainer<OptionLight> optionLights;
Children = new Drawable[]
{
new Container
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Margin = new MarginPadding { Bottom = lights_bottom_margin },
Children = new Drawable[]
{
optionLights = new FillFlowContainer<OptionLight>
{
Margin = new MarginPadding { Bottom = 5 },
Spacing = new Vector2(5, 0),
Direction = FillDirection.Horizontal,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both
},
}
}
};
int optionCount = 0;
int selectedOption = -1;
switch (description.RawValue)
{
case bool val:
optionCount = 1;
if (val) selectedOption = 0;
break;
case Enum _:
var values = Enum.GetValues(description.RawValue.GetType());
optionCount = values.Length;
selectedOption = Convert.ToInt32(description.RawValue);
break;
}
ValueText.Origin = optionCount > 0 ? Anchor.BottomCentre : Anchor.Centre;
for (int i = 0; i < optionCount; i++)
optionLights.Add(new OptionLight { Glowing = i == selectedOption });
}
private class OptionLight : Container
{
private Color4 glowingColour, idleColour;
private const float transition_speed = 300;
private const float glow_strength = 0.4f;
private readonly Box fill;
public OptionLight()
{
Children = new[]
{
fill = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 1,
},
};
}
private bool glowing;
public bool Glowing
{
set
{
glowing = value;
if (!IsLoaded) return;
updateGlow();
}
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
fill.Colour = idleColour = Color4.White.Opacity(0.4f);
glowingColour = Color4.White;
Size = new Vector2(25, 5);
Masking = true;
CornerRadius = 3;
EdgeEffect = new EdgeEffectParameters
{
Colour = colours.BlueDark.Opacity(glow_strength),
Type = EdgeEffectType.Glow,
Radius = 8,
};
}
protected override void LoadComplete()
{
updateGlow();
FinishTransforms(true);
}
private void updateGlow()
{
if (glowing)
{
fill.FadeColour(glowingColour, transition_speed, Easing.OutQuint);
FadeEdgeEffectTo(glow_strength, transition_speed, Easing.OutQuint);
}
else
{
FadeEdgeEffectTo(0, transition_speed, Easing.OutQuint);
fill.FadeColour(idleColour, transition_speed, Easing.OutQuint);
}
}
}
}
}

View File

@ -8,34 +8,25 @@ using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays.OSD;
namespace osu.Game.Overlays
{
/// <summary>
/// An on-screen display which automatically tracks and displays toast notifications for <seealso cref="TrackedSettings"/>.
/// Can also display custom content via <see cref="Display(Toast)"/>
/// </summary>
public class OnScreenDisplay : Container
{
private readonly Container box;
private readonly SpriteText textLine1;
private readonly SpriteText textLine2;
private readonly SpriteText textLine3;
private const float height = 110;
private const float height_notext = 98;
private const float height_contracted = height * 0.9f;
private readonly FillFlowContainer<OptionLight> optionLights;
public OnScreenDisplay()
{
RelativeSizeAxes = Axes.Both;
@ -52,64 +43,6 @@ namespace osu.Game.Overlays
Height = height_contracted,
Alpha = 0,
CornerRadius = 20,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.7f,
},
new Container // purely to add a minimum width
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 240,
RelativeSizeAxes = Axes.Y,
},
textLine1 = new OsuSpriteText
{
Padding = new MarginPadding(10),
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Black),
Spacing = new Vector2(1, 0),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
textLine2 = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light),
Padding = new MarginPadding { Left = 10, Right = 10 },
Anchor = Anchor.Centre,
Origin = Anchor.BottomCentre,
},
new FillFlowContainer
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
optionLights = new FillFlowContainer<OptionLight>
{
Padding = new MarginPadding { Top = 20, Bottom = 5 },
Spacing = new Vector2(5, 0),
Direction = FillDirection.Horizontal,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both
},
textLine3 = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding { Bottom = 15 },
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
Alpha = 0.3f,
},
}
}
}
},
};
}
@ -142,7 +75,7 @@ namespace osu.Game.Overlays
return;
configManager.LoadInto(trackedSettings);
trackedSettings.SettingChanged += display;
trackedSettings.SettingChanged += displayTrackedSettingChange;
trackedConfigManagers.Add((source, configManager), trackedSettings);
}
@ -162,56 +95,23 @@ namespace osu.Game.Overlays
return;
existing.Unload();
existing.SettingChanged -= display;
existing.SettingChanged -= displayTrackedSettingChange;
trackedConfigManagers.Remove((source, configManager));
}
private void display(SettingDescription description)
/// <summary>
/// Displays the provided <see cref="Toast"/> temporarily.
/// </summary>
/// <param name="toast"></param>
public void Display(Toast toast)
{
Schedule(() =>
{
textLine1.Text = description.Name.ToUpperInvariant();
textLine2.Text = description.Value;
textLine3.Text = description.Shortcut.ToUpperInvariant();
if (string.IsNullOrEmpty(textLine3.Text))
textLine3.Text = "NO KEY BOUND";
DisplayTemporarily(box);
int optionCount = 0;
int selectedOption = -1;
switch (description.RawValue)
{
case bool val:
optionCount = 1;
if (val) selectedOption = 0;
break;
case Enum _:
var values = Enum.GetValues(description.RawValue.GetType());
optionCount = values.Length;
selectedOption = Convert.ToInt32(description.RawValue);
break;
}
textLine2.Origin = optionCount > 0 ? Anchor.BottomCentre : Anchor.Centre;
textLine2.Y = optionCount > 0 ? 0 : 5;
if (optionLights.Children.Count != optionCount)
{
optionLights.Clear();
for (int i = 0; i < optionCount; i++)
optionLights.Add(new OptionLight());
}
for (int i = 0; i < optionCount; i++)
optionLights.Children[i].Glowing = i == selectedOption;
});
box.Child = toast;
DisplayTemporarily(box);
}
private void displayTrackedSettingChange(SettingDescription description) => Schedule(() => Display(new TrackedSettingToast(description)));
private TransformSequence<Drawable> fadeIn;
private ScheduledDelegate fadeOut;
@ -236,80 +136,5 @@ namespace osu.Game.Overlays
b => b.ResizeHeightTo(height_contracted, 1500, Easing.InQuint));
}, 500);
}
private class OptionLight : Container
{
private Color4 glowingColour, idleColour;
private const float transition_speed = 300;
private const float glow_strength = 0.4f;
private readonly Box fill;
public OptionLight()
{
Children = new[]
{
fill = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 1,
},
};
}
private bool glowing;
public bool Glowing
{
set
{
glowing = value;
if (!IsLoaded) return;
updateGlow();
}
}
private void updateGlow()
{
if (glowing)
{
fill.FadeColour(glowingColour, transition_speed, Easing.OutQuint);
FadeEdgeEffectTo(glow_strength, transition_speed, Easing.OutQuint);
}
else
{
FadeEdgeEffectTo(0, transition_speed, Easing.OutQuint);
fill.FadeColour(idleColour, transition_speed, Easing.OutQuint);
}
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
fill.Colour = idleColour = Color4.White.Opacity(0.4f);
glowingColour = Color4.White;
Size = new Vector2(25, 5);
Masking = true;
CornerRadius = 3;
EdgeEffect = new EdgeEffectParameters
{
Colour = colours.BlueDark.Opacity(glow_strength),
Type = EdgeEffectType.Glow,
Radius = 8,
};
}
protected override void LoadComplete()
{
updateGlow();
FinishTransforms(true);
}
}
}
}

View File

@ -2,11 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Rulesets;
using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
@ -16,6 +18,8 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
private Color4 accentColour = Color4.White;
public readonly Bindable<User> User = new Bindable<User>();
public ProfileRulesetSelector()
{
TabContainer.Masking = false;
@ -32,24 +36,17 @@ namespace osu.Game.Overlays.Profile.Header.Components
((ProfileRulesetTabItem)tabItem).AccentColour = accentColour;
}
public void SetDefaultRuleset(RulesetInfo ruleset)
protected override void LoadComplete()
{
// Todo: This method shouldn't exist, but bindables don't provide the concept of observing a change to the default value
foreach (TabItem<RulesetInfo> tabItem in TabContainer)
((ProfileRulesetTabItem)tabItem).IsDefault = ((ProfileRulesetTabItem)tabItem).Value.ID == ruleset.ID;
base.LoadComplete();
User.BindValueChanged(u => SetDefaultRuleset(Rulesets.GetRuleset(u.NewValue?.PlayMode ?? "osu")), true);
}
public void SelectDefaultRuleset()
public void SetDefaultRuleset(RulesetInfo ruleset)
{
// Todo: This method shouldn't exist, but bindables don't provide the concept of observing a change to the default value
foreach (TabItem<RulesetInfo> tabItem in TabContainer)
{
if (((ProfileRulesetTabItem)tabItem).IsDefault)
{
Current.Value = ((ProfileRulesetTabItem)tabItem).Value;
return;
}
}
((ProfileRulesetTabItem)tabItem).IsDefault = ((ProfileRulesetTabItem)tabItem).Value.ID == ruleset.ID;
}
protected override TabItem<RulesetInfo> CreateTabItem(RulesetInfo value) => new ProfileRulesetTabItem(value)

View File

@ -31,7 +31,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
private KeyValuePair<int, int>[] ranks;
private int dayIndex;
public Bindable<User> User = new Bindable<User>();
public readonly Bindable<UserStatistics> Statistics = new Bindable<UserStatistics>();
public RankGraph()
{
@ -56,8 +56,6 @@ namespace osu.Game.Overlays.Profile.Header.Components
};
graph.OnBallMove += i => dayIndex = i;
User.ValueChanged += userChanged;
}
[BackgroundDependencyLoader]
@ -66,18 +64,25 @@ namespace osu.Game.Overlays.Profile.Header.Components
graph.LineColour = colours.Yellow;
}
private void userChanged(ValueChangedEvent<User> e)
protected override void LoadComplete()
{
base.LoadComplete();
Statistics.BindValueChanged(statistics => updateStatistics(statistics.NewValue), true);
}
private void updateStatistics(UserStatistics statistics)
{
placeholder.FadeIn(fade_duration, Easing.Out);
if (e.NewValue?.Statistics?.Ranks.Global == null)
if (statistics?.Ranks.Global == null)
{
graph.FadeOut(fade_duration, Easing.Out);
ranks = null;
return;
}
int[] userRanks = e.NewValue.RankHistory?.Data ?? new[] { e.NewValue.Statistics.Ranks.Global.Value };
int[] userRanks = statistics.RankHistory?.Data ?? new[] { statistics.Ranks.Global.Value };
ranks = userRanks.Select((x, index) => new KeyValuePair<int, int>(index, x)).Where(x => x.Value != 0).ToArray();
if (ranks.Length > 1)
@ -191,7 +196,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
}
}
public string TooltipText => User.Value?.Statistics?.Ranks.Global == null ? "" : $"#{ranks[dayIndex].Value:#,##0}|{ranked_days - ranks[dayIndex].Key + 1}";
public string TooltipText => Statistics.Value?.Ranks.Global == null ? "" : $"#{ranks[dayIndex].Value:#,##0}|{ranked_days - ranks[dayIndex].Key + 1}";
public ITooltip GetCustomTooltip() => new RankGraphTooltip();

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