1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-22 19:12:56 +08:00

Merge branch 'master' into mania-hp-bar-position

This commit is contained in:
Salman Alshamrani 2024-12-24 08:30:01 -05:00
commit 855fa4e658
139 changed files with 3992 additions and 815 deletions

View File

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

View File

@ -134,7 +134,6 @@ namespace osu.Desktop
if (iconStream != null)
host.Window.SetIconFromStream(iconStream);
host.Window.CursorState |= CursorState.Hidden;
host.Window.Title = Name;
}

View File

@ -0,0 +1,15 @@
// 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 Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Catch.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameApplicationDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}

View File

@ -1,16 +1,15 @@
// 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.iOS;
using osu.Game.Tests;
using UIKit;
namespace osu.Game.Rulesets.Catch.Tests.iOS
{
public static class Application
public static class Program
{
public static void Main(string[] args)
{
GameApplication.Main(new OsuTestBrowser());
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}

View File

@ -0,0 +1,15 @@
// 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 Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Mania.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameApplicationDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}

View File

@ -1,16 +1,15 @@
// 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.iOS;
using osu.Game.Tests;
using UIKit;
namespace osu.Game.Rulesets.Mania.Tests.iOS
{
public static class Application
public static class Program
{
public static void Main(string[] args)
{
GameApplication.Main(new OsuTestBrowser());
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}

View File

@ -22,9 +22,11 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase("basic")]
[TestCase("zero-length-slider")]
[TestCase("mania-specific-spinner")]
[TestCase("20544")]
[TestCase("100374")]
[TestCase("1450162")]
[TestCase("4869637")]
public void Test(string name) => base.Test(name);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,60 @@
{
"Mappings": [
{
"RandomW": 273071671,
"RandomX": 842502087,
"RandomY": 3579807591,
"RandomZ": 273326509,
"StartTime": 11783.0,
"Objects": [
{
"StartTime": 11783.0,
"EndTime": 15116.0,
"Column": 0
}
]
},
{
"RandomW": 2659271247,
"RandomX": 3579807591,
"RandomY": 273326509,
"RandomZ": 273071671,
"StartTime": 91545.0,
"Objects": [
{
"StartTime": 91545.0,
"EndTime": 92735.0,
"Column": 0
}
]
},
{
"RandomW": 3083635271,
"RandomX": 273326509,
"RandomY": 273071671,
"RandomZ": 2659271247,
"StartTime": 152497.0,
"Objects": [
{
"StartTime": 152497.0,
"EndTime": 153687.0,
"Column": 1
}
]
},
{
"RandomW": 4073591514,
"RandomX": 273071671,
"RandomY": 2659271247,
"RandomZ": 3083635271,
"StartTime": 231545.0,
"Objects": [
{
"StartTime": 231545.0,
"EndTime": 232974.0,
"Column": 3
}
]
}
]
}

View File

@ -0,0 +1,27 @@
osu file format v14
[General]
Mode: 3
[Difficulty]
HPDrainRate:5
CircleSize:4
OverallDifficulty:5
ApproachRate:0
SliderMultiplier:2.6
SliderTickRate:1
[TimingPoints]
355,476.190476190476,4,2,1,60,1,0
60652,-100,4,2,1,60,0,1
92735,-100,4,2,1,60,0,0
121485,-100,4,2,1,60,0,1
153688,-100,4,2,1,60,0,0
182497,-100,4,2,1,60,0,1
213688,-100,4,2,1,60,0,0
[HitObjects]
256,192,11783,12,0,15116,0:0:0:0:
256,192,91545,12,0,92735,0:0:0:0:
256,192,152497,12,0,153687,0:0:0:0:
256,192,231545,12,0,232974,0:0:0:0:

View File

@ -7,11 +7,13 @@ using System.Linq;
using System.Collections.Generic;
using System.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Utils;
using osuTK;
@ -124,16 +126,109 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
{
if (original is ManiaHitObject maniaOriginal)
{
yield return maniaOriginal;
LegacyHitObjectType legacyType;
yield break;
switch (original)
{
case ManiaHitObject maniaObj:
{
yield return maniaObj;
yield break;
}
case IHasLegacyHitObjectType legacy:
legacyType = legacy.LegacyType & LegacyHitObjectType.ObjectTypes;
break;
case IHasPath:
legacyType = LegacyHitObjectType.Slider;
break;
case IHasDuration:
legacyType = LegacyHitObjectType.Hold;
break;
default:
legacyType = LegacyHitObjectType.Circle;
break;
}
var objects = IsForCurrentRuleset ? generateSpecific(original, beatmap) : generateConverted(original, beatmap);
foreach (ManiaHitObject obj in objects)
yield return obj;
double startTime = original.StartTime;
double endTime = (original as IHasDuration)?.EndTime ?? startTime;
Vector2 position = (original as IHasPosition)?.Position ?? Vector2.Zero;
PatternGenerator conversion;
switch (legacyType)
{
case LegacyHitObjectType.Circle:
if (IsForCurrentRuleset)
{
conversion = new PassThroughPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern);
recordNote(startTime, position);
}
else
{
// Note: The density is used during the pattern generator constructor, and intentionally computed first.
computeDensity(startTime);
conversion = new HitCirclePatternGenerator(Random, original, beatmap, TotalColumns, lastPattern, lastTime, lastPosition, density, lastStair);
recordNote(startTime, position);
}
break;
case LegacyHitObjectType.Slider:
if (IsForCurrentRuleset)
{
conversion = new PassThroughPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern);
recordNote(original.StartTime, position);
}
else
{
var generator = new SliderPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern);
conversion = generator;
for (int i = 0; i <= generator.SpanCount; i++)
{
double time = original.StartTime + generator.SegmentDuration * i;
recordNote(time, position);
computeDensity(time);
}
}
break;
case LegacyHitObjectType.Spinner:
// Note: Some older mania-specific beatmaps can have spinners that are converted rather than passed through.
// Newer beatmaps will usually use the "hold" hitobject type below.
conversion = new SpinnerPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern);
recordNote(endTime, new Vector2(256, 192));
computeDensity(endTime);
break;
case LegacyHitObjectType.Hold:
conversion = new PassThroughPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern);
recordNote(endTime, position);
computeDensity(endTime);
break;
default:
throw new ArgumentException($"Invalid legacy object type: {legacyType}", nameof(original));
}
foreach (var newPattern in conversion.Generate())
{
if (conversion is HitCirclePatternGenerator circleGenerator)
lastStair = circleGenerator.StairType;
if (conversion is HitCirclePatternGenerator || conversion is SliderPatternGenerator)
lastPattern = newPattern;
foreach (var obj in newPattern.HitObjects)
yield return obj;
}
}
private readonly LimitedCapacityQueue<double> prevNoteTimes = new LimitedCapacityQueue<double>(max_notes_for_density);
@ -156,135 +251,5 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
lastTime = time;
lastPosition = position;
}
/// <summary>
/// Method that generates hit objects for osu!mania specific beatmaps.
/// </summary>
/// <param name="original">The original hit object.</param>
/// <param name="originalBeatmap">The original beatmap. This is used to look-up any values dependent on a fully-loaded beatmap.</param>
/// <returns>The hit objects generated.</returns>
private IEnumerable<ManiaHitObject> generateSpecific(HitObject original, IBeatmap originalBeatmap)
{
var generator = new SpecificBeatmapPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
foreach (var newPattern in generator.Generate())
{
lastPattern = newPattern;
foreach (var obj in newPattern.HitObjects)
yield return obj;
}
}
/// <summary>
/// Method that generates hit objects for non-osu!mania beatmaps.
/// </summary>
/// <param name="original">The original hit object.</param>
/// <param name="originalBeatmap">The original beatmap. This is used to look-up any values dependent on a fully-loaded beatmap.</param>
/// <returns>The hit objects generated.</returns>
private IEnumerable<ManiaHitObject> generateConverted(HitObject original, IBeatmap originalBeatmap)
{
Patterns.PatternGenerator? conversion = null;
switch (original)
{
case IHasPath:
{
var generator = new PathObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
conversion = generator;
var positionData = original as IHasPosition;
for (int i = 0; i <= generator.SpanCount; i++)
{
double time = original.StartTime + generator.SegmentDuration * i;
recordNote(time, positionData?.Position ?? Vector2.Zero);
computeDensity(time);
}
break;
}
case IHasDuration endTimeData:
{
conversion = new EndTimeObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
recordNote(endTimeData.EndTime, new Vector2(256, 192));
computeDensity(endTimeData.EndTime);
break;
}
case IHasPosition positionData:
{
computeDensity(original.StartTime);
conversion = new HitObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern, lastTime, lastPosition, density, lastStair);
recordNote(original.StartTime, positionData.Position);
break;
}
}
if (conversion == null)
yield break;
foreach (var newPattern in conversion.Generate())
{
lastPattern = conversion is EndTimeObjectPatternGenerator ? lastPattern : newPattern;
lastStair = (conversion as HitObjectPatternGenerator)?.StairType ?? lastStair;
foreach (var obj in newPattern.HitObjects)
yield return obj;
}
}
/// <summary>
/// A pattern generator for osu!mania-specific beatmaps.
/// </summary>
private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator
{
public SpecificBeatmapPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
: base(random, hitObject, beatmap, previousPattern, totalColumns)
{
}
public override IEnumerable<Pattern> Generate()
{
yield return generate();
}
private Pattern generate()
{
var positionData = HitObject as IHasXPosition;
int column = GetColumn(positionData?.X ?? 0);
var pattern = new Pattern();
if (HitObject is IHasDuration endTimeData)
{
pattern.Add(new HoldNote
{
StartTime = HitObject.StartTime,
Duration = endTimeData.Duration,
Column = column,
Samples = HitObject.Samples,
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject)
});
}
else if (HitObject is IHasXPosition)
{
pattern.Add(new Note
{
StartTime = HitObject.StartTime,
Samples = HitObject.Samples,
Column = column
});
}
return pattern;
}
}
}
}

View File

@ -16,13 +16,16 @@ using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
internal class HitObjectPatternGenerator : PatternGenerator
/// <summary>
/// Converter for legacy "HitCircle" hit objects.
/// </summary>
internal class HitCirclePatternGenerator : LegacyPatternGenerator
{
public PatternType StairType { get; private set; }
private readonly PatternType convertType;
public HitObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern, double previousTime, Vector2 previousPosition,
public HitCirclePatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern, double previousTime, Vector2 previousPosition,
double density, PatternType lastStair)
: base(random, hitObject, beatmap, previousPattern, totalColumns)
{
@ -114,10 +117,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
}
if (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1
// If we convert to 7K + 1, let's not overload the special key
&& (TotalColumns != 8 || lastColumn != 0)
// Make sure the last column was not the centre column
&& (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2))
// If we convert to 7K + 1, let's not overload the special key
&& (TotalColumns != 8 || lastColumn != 0)
// Make sure the last column was not the centre column
&& (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2))
{
// Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object)
int column = RandomStart + TotalColumns - lastColumn - 1;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Linq;
using JetBrains.Annotations;
@ -15,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <summary>
/// A pattern generator for legacy hit objects.
/// </summary>
internal abstract class PatternGenerator : Patterns.PatternGenerator
internal abstract class LegacyPatternGenerator : PatternGenerator
{
/// <summary>
/// The column index at which to start generating random notes.
@ -27,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary>
protected readonly LegacyRandom Random;
protected PatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, Pattern previousPattern, int totalColumns)
protected LegacyPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, Pattern previousPattern, int totalColumns)
: base(hitObject, beatmap, totalColumns, previousPattern)
{
ArgumentNullException.ThrowIfNull(random);
@ -96,8 +94,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (conversionDifficulty != null)
return conversionDifficulty.Value;
HitObject lastObject = Beatmap.HitObjects.LastOrDefault();
HitObject firstObject = Beatmap.HitObjects.FirstOrDefault();
HitObject? lastObject = Beatmap.HitObjects.LastOrDefault();
HitObject? firstObject = Beatmap.HitObjects.FirstOrDefault();
// Drain time in seconds
int drainTime = (int)(((lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0) - Beatmap.TotalBreakTime) / 1000);
@ -132,13 +130,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <param name="nextColumn">A function to retrieve the next column. If null, a randomisation scheme will be used.</param>
/// <param name="validation">A function to perform additional validation checks to determine if a column is a valid candidate for a <see cref="HitObject"/>.</param>
/// <param name="lowerBound">The minimum column index. If null, <see cref="RandomStart"/> is used.</param>
/// <param name="upperBound">The maximum column index. If null, <see cref="Patterns.PatternGenerator.TotalColumns">TotalColumns</see> is used.</param>
/// <param name="upperBound">The maximum column index. If null, <see cref="PatternGenerator.TotalColumns">TotalColumns</see> is used.</param>
/// <param name="patterns">A list of patterns for which the validity of a column should be checked against.
/// A column is not a valid candidate if a <see cref="HitObject"/> occupies the same column in any of the patterns.</param>
/// <returns>A column which has passed the <paramref name="validation"/> check and for which there are no
/// <see cref="HitObject"/>s in any of <paramref name="patterns"/> occupying the same column.</returns>
/// <exception cref="NotEnoughColumnsException">If there are no valid candidate columns.</exception>
protected int FindAvailableColumn(int initialColumn, int? lowerBound = null, int? upperBound = null, Func<int, int> nextColumn = null, [InstantHandle] Func<int, bool> validation = null,
protected int FindAvailableColumn(int initialColumn, int? lowerBound = null, int? upperBound = null, Func<int, int>? nextColumn = null, [InstantHandle] Func<int, bool>? validation = null,
params Pattern[] patterns)
{
lowerBound ??= RandomStart;
@ -189,7 +187,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// Returns a random column index in the range [<paramref name="lowerBound"/>, <paramref name="upperBound"/>).
/// </summary>
/// <param name="lowerBound">The minimum column index. If null, <see cref="RandomStart"/> is used.</param>
/// <param name="upperBound">The maximum column index. If null, <see cref="Patterns.PatternGenerator.TotalColumns"/> is used.</param>
/// <param name="upperBound">The maximum column index. If null, <see cref="PatternGenerator.TotalColumns"/> is used.</param>
protected int GetRandomColumn(int? lowerBound = null, int? upperBound = null) => Random.Next(lowerBound ?? RandomStart, upperBound ?? TotalColumns);
/// <summary>

View File

@ -0,0 +1,55 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
/// <summary>
/// A simple generator which, for any object, if the hitobject has an end time
/// it becomes a <see cref="HoldNote"/> or otherwise a <see cref="Note"/>.
/// </summary>
internal class PassThroughPatternGenerator : LegacyPatternGenerator
{
public PassThroughPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
: base(random, hitObject, beatmap, previousPattern, totalColumns)
{
}
public override IEnumerable<Pattern> Generate()
{
var positionData = HitObject as IHasXPosition;
int column = GetColumn(positionData?.X ?? 0);
var pattern = new Pattern();
if (HitObject is IHasDuration endTimeData)
{
pattern.Add(new HoldNote
{
StartTime = HitObject.StartTime,
Duration = endTimeData.Duration,
Column = column,
Samples = HitObject.Samples,
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject)
});
}
else
{
pattern.Add(new Note
{
StartTime = HitObject.StartTime,
Samples = HitObject.Samples,
Column = column
});
}
yield return pattern;
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
@ -19,9 +17,9 @@ using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
/// <summary>
/// A pattern generator for IHasDistance hit objects.
/// Converter for legacy "Slider" hit objects.
/// </summary>
internal class PathObjectPatternGenerator : PatternGenerator
internal class SliderPatternGenerator : LegacyPatternGenerator
{
public readonly int StartTime;
public readonly int EndTime;
@ -30,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private PatternType convertType;
public PathObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
public SliderPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
: base(random, hitObject, beatmap, previousPattern, totalColumns)
{
convertType = PatternType.None;
@ -484,9 +482,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// Retrieves the list of node samples that occur at time greater than or equal to <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to retrieve node samples at.</param>
private IList<IList<HitSampleInfo>> nodeSamplesAt(int time)
private IList<IList<HitSampleInfo>>? nodeSamplesAt(int time)
{
if (!(HitObject is IHasPathWithRepeats curveData))
if (HitObject is not IHasPathWithRepeats curveData)
return null;
int index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;

View File

@ -12,12 +12,15 @@ using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
internal class EndTimeObjectPatternGenerator : PatternGenerator
/// <summary>
/// Converter for legacy "Spinner" hit objects.
/// </summary>
internal class SpinnerPatternGenerator : LegacyPatternGenerator
{
private readonly int endTime;
private readonly PatternType convertType;
public EndTimeObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
public SpinnerPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
: base(random, hitObject, beatmap, previousPattern, totalColumns)
{
endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0);

View File

@ -1,9 +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.
#nullable disable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using osu.Game.Rulesets.Mania.Objects;
@ -14,8 +13,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
/// </summary>
internal class Pattern
{
private List<ManiaHitObject> hitObjects;
private HashSet<int> containedColumns;
private List<ManiaHitObject>? hitObjects;
private HashSet<int>? containedColumns;
/// <summary>
/// All the hit objects contained in this pattern.
@ -72,6 +71,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
containedColumns?.Clear();
}
[MemberNotNull(nameof(hitObjects), nameof(containedColumns))]
private void prepareStorage()
{
hitObjects ??= new List<ManiaHitObject>();

View File

@ -0,0 +1,15 @@
// 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 Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Osu.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameApplicationDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}

View File

@ -1,16 +1,15 @@
// 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.iOS;
using osu.Game.Tests;
using UIKit;
namespace osu.Game.Rulesets.Osu.Tests.iOS
{
public static class Application
public static class Program
{
public static void Main(string[] args)
{
GameApplication.Main(new OsuTestBrowser());
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}

View File

@ -76,6 +76,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
base.OnDragEnd(e);
}
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnClick(ClickEvent e) => true;
private void updateState()
{
Colour = IsHovered || IsDragged ? colours.Red : colours.Yellow;

View File

@ -61,13 +61,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
var drawableOsuObject = (DrawableOsuHitObject?)drawableObject;
// As a precondition, ensure that any prefix lookups are run against the skin which is providing "hitcircle".
// As a precondition, prefer that any *prefix* lookups are run against the skin which is providing "hitcircle".
// This is to correctly handle a case such as:
//
// - Beatmap provides `hitcircle`
// - User skin provides `sliderstartcircle`
//
// In such a case, the `hitcircle` should be used for slider start circles rather than the user's skin override.
//
// Of note, this consideration should only be used to decide whether to continue looking up the prefixed name or not.
// The final lookups must still run on the full skin hierarchy as per usual in order to correctly handle fallback cases.
var provider = skin.FindProvider(s => s.GetTexture(base_lookup) != null) ?? skin;
// if a base texture for the specified prefix exists, continue using it for subsequent lookups.
@ -81,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
InternalChildren = new[]
{
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = provider.GetTexture(circleName)?.WithMaximumSize(maxSize) })
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(maxSize) })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = provider.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) })
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.UI
protected override ResumeOverlay CreateResumeOverlay()
{
if (Mods.Any(m => m is OsuModAutopilot))
if (Mods.Any(m => m is OsuModAutopilot or OsuModTouchDevice))
return new DelayedResumeOverlay { Scale = new Vector2(0.65f) };
return new OsuResumeOverlay();

View File

@ -0,0 +1,15 @@
// 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 Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Taiko.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameApplicationDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}

View File

@ -1,16 +1,15 @@
// 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.iOS;
using osu.Game.Tests;
using UIKit;
namespace osu.Game.Rulesets.Taiko.Tests.iOS
{
public static class Application
public static class Program
{
public static void Main(string[] args)
{
GameApplication.Main(new OsuTestBrowser());
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}

View File

@ -0,0 +1,14 @@
// 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 Foundation;
using osu.Framework.iOS;
namespace osu.Game.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameApplicationDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}

View File

@ -1,15 +1,15 @@
// 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.iOS;
using UIKit;
namespace osu.Game.Tests.iOS
{
public static class Application
public static class Program
{
public static void Main(string[] args)
{
GameApplication.Main(new OsuTestBrowser());
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}

View File

@ -67,7 +67,9 @@ namespace osu.Game.Tests.Skins
// Covers legacy rank display
"Archives/modified-classic-20230809.osk",
// Covers legacy key counter
"Archives/modified-classic-20240724.osk"
"Archives/modified-classic-20240724.osk",
// Covers skinnable mod display
"Archives/modified-default-20241207.osk"
};
[Resolved]

View File

@ -131,21 +131,6 @@ namespace osu.Game.Tests.Visual.Background
assertNoBackgrounds();
}
[Test]
public void TestDelayedConnectivity()
{
registerBackgroundsResponse(DateTimeOffset.Now.AddDays(30));
setSeasonalBackgroundMode(SeasonalBackgroundMode.Always);
AddStep("go offline", () => dummyAPI.SetState(APIState.Offline));
createLoader();
assertNoBackgrounds();
AddStep("go online", () => dummyAPI.SetState(APIState.Online));
assertAnyBackground();
}
private void registerBackgroundsResponse(DateTimeOffset endDate)
=> AddStep("setup request handler", () =>
{
@ -185,7 +170,8 @@ namespace osu.Game.Tests.Visual.Background
{
previousBackground = (SeasonalBackground)backgroundContainer.SingleOrDefault();
background = backgroundLoader.LoadNextBackground();
LoadComponentAsync(background, bg => backgroundContainer.Child = bg);
if (background != null)
LoadComponentAsync(background, bg => backgroundContainer.Child = bg);
});
AddUntilStep("background loaded", () => background.IsLoaded);

View File

@ -203,12 +203,19 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestCreateNewDifficultyWithScrollSpeed_SameRuleset()
{
string firstDifficultyName = Guid.NewGuid().ToString();
string previousDifficultyName = null!;
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = previousDifficultyName = Guid.NewGuid().ToString());
AddStep("save beatmap", () => Editor.Save());
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new ManiaRuleset().RulesetInfo));
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName);
AddUntilStep("wait for created", () =>
{
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != previousDifficultyName;
});
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = previousDifficultyName = Guid.NewGuid().ToString());
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
AddStep("add effect points", () =>
{
@ -229,7 +236,7 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("wait for created", () =>
{
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != firstDifficultyName;
return difficultyName != null && difficultyName != previousDifficultyName;
});
AddAssert("created difficulty has timing point", () =>

View File

@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Editing
private TimingScreen timingScreen;
private EditorBeatmap editorBeatmap;
private BeatmapEditorChangeHandler changeHandler;
protected override bool ScrollUsingMouseWheel => false;
@ -46,6 +47,7 @@ namespace osu.Game.Tests.Visual.Editing
private void reloadEditorBeatmap()
{
editorBeatmap = new EditorBeatmap(Beatmap.Value.GetPlayableBeatmap(Ruleset.Value));
changeHandler = new BeatmapEditorChangeHandler(editorBeatmap);
Child = new DependencyProvidingContainer
{
@ -53,6 +55,7 @@ namespace osu.Game.Tests.Visual.Editing
CachedDependencies = new (Type, object)[]
{
(typeof(EditorBeatmap), editorBeatmap),
(typeof(IEditorChangeHandler), changeHandler),
(typeof(IBeatSnapProvider), editorBeatmap)
},
Child = timingScreen = new TimingScreen
@ -72,8 +75,10 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("Wait for rows to load", () => Child.ChildrenOfType<EffectRowAttribute>().Any());
}
// TODO: this is best-effort for now, but the comment out test below should probably be how things should work.
// Was originally working as of https://github.com/ppy/osu/pull/26141; Regressed at some point.
[Test]
public void TestSelectedRetainedOverUndo()
public void TestSelectionDismissedOnUndo()
{
AddStep("Select first timing point", () =>
{
@ -95,25 +100,52 @@ namespace osu.Game.Tests.Visual.Editing
return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170;
});
AddStep("simulate undo", () =>
{
var clone = editorBeatmap.ControlPointInfo.DeepClone();
AddStep("undo", () => changeHandler?.RestoreState(-1));
editorBeatmap.ControlPointInfo.Clear();
foreach (var group in clone.Groups)
{
foreach (var cp in group.ControlPoints)
editorBeatmap.ControlPointInfo.Add(group.Time, cp);
}
});
AddUntilStep("selection retained", () =>
{
return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170;
});
AddUntilStep("selection dismissed", () => timingScreen.SelectedGroup.Value, () => Is.Null);
}
// [Test]
// public void TestSelectedRetainedOverUndo()
// {
// AddStep("Select first timing point", () =>
// {
// InputManager.MoveMouseTo(Child.ChildrenOfType<TimingRowAttribute>().First());
// InputManager.Click(MouseButton.Left);
// });
//
// AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 2170);
// AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 2170);
//
// AddStep("Adjust offset", () =>
// {
// InputManager.MoveMouseTo(timingScreen.ChildrenOfType<TimingAdjustButton>().First().ScreenSpaceDrawQuad.Centre + new Vector2(20, 0));
// InputManager.Click(MouseButton.Left);
// });
//
// AddUntilStep("wait for offset changed", () =>
// {
// return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170;
// });
//
// AddStep("undo", () => changeHandler?.RestoreState(-1));
//
// AddUntilStep("selection retained", () =>
// {
// return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170;
// });
//
// AddAssert("check group count", () => editorBeatmap.ControlPointInfo.Groups.Count, () => Is.EqualTo(10));
//
// AddStep("Adjust offset", () =>
// {
// InputManager.MoveMouseTo(timingScreen.ChildrenOfType<TimingAdjustButton>().First().ScreenSpaceDrawQuad.Centre + new Vector2(20, 0));
// InputManager.Click(MouseButton.Left);
// });
//
// AddAssert("check group count", () => editorBeatmap.ControlPointInfo.Groups.Count, () => Is.EqualTo(10));
// }
[Test]
public void TestScrollControlGroupIntoView()
{

View File

@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
@ -19,6 +20,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osu.Game.Storyboards;
using osuTK;
using osuTK.Input;
@ -28,6 +30,12 @@ namespace osu.Game.Tests.Visual.Gameplay
{
protected new PausePlayer Player => (PausePlayer)base.Player;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{
beatmap.AudioLeadIn = 4000;
return base.CreateWorkingBeatmap(beatmap, storyboard);
}
private readonly Container content;
protected override Container<Drawable> Content => content;
@ -200,8 +208,10 @@ namespace osu.Game.Tests.Visual.Gameplay
}
[Test]
[Ignore("Fails on github runners if they happen to skip too far forward in time.")]
public void TestUserPauseDuringCooldownTooSoon()
{
AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0));
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
pauseAndConfirm();
@ -213,9 +223,23 @@ namespace osu.Game.Tests.Visual.Gameplay
confirmNotExited();
}
[Test]
public void TestUserPauseDuringIntroSkipsCooldown()
{
AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000));
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
pauseAndConfirm();
resume();
pauseViaBackAction();
confirmPaused();
}
[Test]
public void TestQuickExitDuringCooldownTooSoon()
{
AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0));
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
pauseAndConfirm();

View File

@ -0,0 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Screens.Menu;
using osu.Game.Seasonal;
namespace osu.Game.Tests.Visual.Menus
{
[TestFixture]
public partial class TestSceneIntroChristmas : IntroTestScene
{
protected override bool IntroReliesOnTrack => true;
protected override IntroScreen CreateScreen() => new IntroChristmas();
}
}

View File

@ -0,0 +1,43 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Seasonal;
namespace osu.Game.Tests.Visual.Menus
{
public partial class TestSceneMainMenuSeasonalLighting : OsuTestScene
{
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("prepare beatmap", () =>
{
var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == IntroChristmas.CHRISTMAS_BEATMAP_SET_HASH);
if (setInfo != null)
Beatmap.Value = beatmaps.GetWorkingBeatmap(setInfo.Value.Beatmaps.First());
});
AddStep("create lighting", () => Child = new MainMenuSeasonalLighting());
AddStep("restart beatmap", () =>
{
Beatmap.Value.Track.Start();
Beatmap.Value.Track.Seek(4000);
});
}
[Test]
public void TestBasic()
{
}
}
}

View File

@ -14,7 +14,6 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Lounge;
@ -76,7 +75,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
createLoungeRoom(new Room
{
Name = "Multiplayer room",
Status = new RoomStatusOpen(),
EndDate = DateTimeOffset.Now.AddDays(1),
Type = MatchType.HeadToHead,
Playlist = [item1],
@ -85,7 +83,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
createLoungeRoom(new Room
{
Name = "Private room",
Status = new RoomStatusOpenPrivate(),
Password = "*",
EndDate = DateTimeOffset.Now.AddDays(1),
Type = MatchType.HeadToHead,
@ -95,36 +92,38 @@ namespace osu.Game.Tests.Visual.Multiplayer
createLoungeRoom(new Room
{
Name = "Playlist room with multiple beatmaps",
Status = new RoomStatusPlaying(),
Status = RoomStatus.Playing,
EndDate = DateTimeOffset.Now.AddDays(1),
Playlist = [item1, item2],
CurrentPlaylistItem = item1
}),
createLoungeRoom(new Room
{
Name = "Finished room",
Status = new RoomStatusEnded(),
Name = "Closing soon",
EndDate = DateTimeOffset.Now.AddSeconds(5),
}),
createLoungeRoom(new Room
{
Name = "Closed room",
EndDate = DateTimeOffset.Now,
}),
createLoungeRoom(new Room
{
Name = "Spotlight room",
Status = new RoomStatusOpen(),
Category = RoomCategory.Spotlight,
}),
createLoungeRoom(new Room
{
Name = "Featured artist room",
Status = new RoomStatusOpen(),
Category = RoomCategory.FeaturedArtist,
}),
}
};
});
AddUntilStep("wait for panel load", () => rooms.Count == 6);
AddUntilStep("wait for panel load", () => rooms.Count == 7);
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)) == 2);
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 4);
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 5);
}
[Test]
@ -136,7 +135,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create room", () => Child = drawableRoom = createLoungeRoom(room = new Room
{
Name = "Room with password",
Status = new RoomStatusOpen(),
Type = MatchType.HeadToHead,
}));

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Game.Configuration;
@ -58,7 +56,11 @@ namespace osu.Game.Tests.Visual.Navigation
// First scroll makes volume controls appear, second adjusts volume.
AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 10);
AddAssert("Volume is still zero", () => Game.Audio.Volume.Value == 0);
AddAssert("Volume is still zero", () => Game.Audio.Volume.Value, () => Is.Zero);
AddStep("Pause", () => InputManager.PressKey(Key.Escape));
AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 10);
AddAssert("Volume is above zero", () => Game.Audio.Volume.Value > 0);
}
[Test]
@ -80,8 +82,8 @@ namespace osu.Game.Tests.Visual.Navigation
private void loadToPlayerNonBreakTime()
{
Player player = null;
Screens.Select.SongSelect songSelect = null;
Player? player = null;
Screens.Select.SongSelect songSelect = null!;
PushAndConfirm(() => songSelect = new TestSceneScreenNavigation.TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
@ -95,7 +97,7 @@ namespace osu.Game.Tests.Visual.Navigation
return (player = Game.ScreenStack.CurrentScreen as Player) != null;
});
AddUntilStep("wait for play time active", () => !player.IsBreakTime.Value);
AddUntilStep("wait for play time active", () => player!.IsBreakTime.Value, () => Is.False);
}
}
}

View File

@ -23,6 +23,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps.IO;
@ -212,6 +213,33 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any());
}
[Test]
public void TestGameplaySettingsDoesNotExpandWhenSkinOverlayPresent()
{
advanceToSongSelect();
openSkinEditor();
AddStep("select autoplay", () => Game.SelectedMods.Value = new Mod[] { new OsuModAutoplay() });
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
switchToGameplayScene();
AddUntilStep("wait for settings", () => getPlayerSettingsOverlay() != null);
AddAssert("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0));
AddStep("move cursor to right of screen", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight));
AddAssert("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0));
toggleSkinEditor();
AddStep("move cursor slightly", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(1)));
AddUntilStep("settings visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.GreaterThan(0));
AddStep("move cursor to right of screen too far", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(10240, 0)));
AddUntilStep("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0));
PlayerSettingsOverlay getPlayerSettingsOverlay() => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PlayerSettingsOverlay>().SingleOrDefault();
}
[Test]
public void TestCinemaModRemovedOnEnteringGameplay()
{

View File

@ -1,149 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Playlists
{
public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene
{
private const double track_length = 10000;
[Resolved]
private IAPIProvider api { get; set; } = null!;
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
private BeatmapManager beatmaps = null!;
private RulesetStore rulesets = null!;
private BeatmapSetInfo? importedSet;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new ScoreManager(rulesets, () => beatmaps, LocalStorage, Realm, API));
Dependencies.Cache(Realm);
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
Realm.Write(r =>
{
foreach (var set in r.All<BeatmapSetInfo>())
{
foreach (var b in set.Beatmaps)
{
// These will all have a virtual track length of 1000, see WorkingBeatmap.GetVirtualTrack().
b.Length = track_length - 1000;
}
}
});
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
}
[Test]
public void TestStatusUpdateOnEnter()
{
Room room = null!;
PlaylistsRoomSubScreen roomScreen = null!;
AddStep("create room", () =>
{
RoomManager.AddRoom(room = new Room
{
Name = @"Test Room",
Host = new APIUser { Username = @"Host" },
Category = RoomCategory.Normal,
EndDate = DateTimeOffset.Now.AddMinutes(-1)
});
});
AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room)));
AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen());
AddAssert("status is still ended", () => roomScreen.Room.Status, Is.TypeOf<RoomStatusEnded>);
}
[Test]
public void TestCloseButtonGoesAwayAfterGracePeriod()
{
Room room = null!;
PlaylistsRoomSubScreen roomScreen = null!;
AddStep("create room", () =>
{
RoomManager.AddRoom(room = new Room
{
Name = @"Test Room",
Host = api.LocalUser.Value,
Category = RoomCategory.Normal,
StartDate = DateTimeOffset.Now.AddMinutes(-5).AddSeconds(3),
EndDate = DateTimeOffset.Now.AddMinutes(30)
});
});
AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room)));
AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen());
AddAssert("close button present", () => roomScreen.ChildrenOfType<DangerousRoundedButton>().Any());
AddUntilStep("wait for close button to disappear", () => !roomScreen.ChildrenOfType<DangerousRoundedButton>().Any());
}
[TestCase(120_000, true)] // Definitely enough time.
[TestCase(45_000, true)] // Enough time.
[TestCase(35_000, false)] // Not enough time to complete beatmap after lenience.
[TestCase(20_000, false)] // Not enough time.
[TestCase(5_000, false)] // Not enough time to complete beatmap before lenience.
[TestCase(37_500, true, 2)] // Enough time to complete beatmap after mods are applied.
public void TestReadyButtonEnablementPeriod(int offsetMs, bool enabled, double rate = 1)
{
Room room = null!;
PlaylistsRoomSubScreen roomScreen = null!;
AddStep("create room", () =>
{
RoomManager.AddRoom(room = new Room
{
Name = @"Test Room",
Host = api.LocalUser.Value,
Category = RoomCategory.Normal,
StartDate = DateTimeOffset.Now,
EndDate = DateTimeOffset.Now.AddMilliseconds(offsetMs),
Playlist =
[
new PlaylistItem(importedSet!.Beatmaps[0])
{
RequiredMods = rate == 1
? []
: [new APIMod(new OsuModDoubleTime { SpeedChange = { Value = rate } })]
}
]
});
});
AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room)));
AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen());
AddUntilStep("ready button enabled", () => roomScreen.ChildrenOfType<PlaylistsReadyButton>().SingleOrDefault()?.Enabled.Value, () => Is.EqualTo(enabled));
}
}
}

View File

@ -13,9 +13,12 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
@ -23,6 +26,8 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osu.Game.Utils;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
{
@ -63,7 +68,7 @@ namespace osu.Game.Tests.Visual.SongSelect
return 336; // recommended star rating of 2
case 1:
return 928; // SR 3
return 973; // SR 3
case 2:
return 1905; // SR 4
@ -170,6 +175,45 @@ namespace osu.Game.Tests.Visual.SongSelect
presentAndConfirm(() => maniaSet, 5);
}
[Test]
public void TestBeatmapListingFilter()
{
AddStep("set playmode to taiko", () => ((DummyAPIAccess)API).LocalUser.Value.PlayMode = "taiko");
AddStep("open beatmap listing", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressKey(Key.B);
InputManager.ReleaseKey(Key.B);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("wait for load", () => Game.ChildrenOfType<BeatmapListingOverlay>().SingleOrDefault()?.IsLoaded, () => Is.True);
checkRecommendedDifficulty(3);
AddStep("change mode filter to osu!", () => Game.ChildrenOfType<BeatmapSearchRulesetFilterRow>().Single().ChildrenOfType<FilterTabItem<RulesetInfo>>().ElementAt(1).TriggerClick());
checkRecommendedDifficulty(2);
AddStep("change mode filter to osu!taiko", () => Game.ChildrenOfType<BeatmapSearchRulesetFilterRow>().Single().ChildrenOfType<FilterTabItem<RulesetInfo>>().ElementAt(2).TriggerClick());
checkRecommendedDifficulty(3);
AddStep("change mode filter to osu!catch", () => Game.ChildrenOfType<BeatmapSearchRulesetFilterRow>().Single().ChildrenOfType<FilterTabItem<RulesetInfo>>().ElementAt(3).TriggerClick());
checkRecommendedDifficulty(4);
AddStep("change mode filter to osu!mania", () => Game.ChildrenOfType<BeatmapSearchRulesetFilterRow>().Single().ChildrenOfType<FilterTabItem<RulesetInfo>>().ElementAt(4).TriggerClick());
checkRecommendedDifficulty(5);
void checkRecommendedDifficulty(double starRating)
=> AddAssert($"recommended difficulty is {starRating}",
() => Game.ChildrenOfType<BeatmapSearchGeneralFilterRow>().Single().ChildrenOfType<OsuSpriteText>().ElementAt(1).Text.ToString(),
() => Is.EqualTo($"Recommended difficulty ({starRating.FormatStarRating()})"));
}
private BeatmapSetInfo importBeatmapSet(IEnumerable<RulesetInfo> difficultyRulesets)
{
var rulesets = difficultyRulesets.ToArray();

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelectV2
{

View File

@ -4,20 +4,52 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.Menu;
using osu.Game.Seasonal;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneOsuLogo : OsuTestScene
{
private OsuLogo? logo;
private float scale = 1;
protected override void LoadComplete()
{
base.LoadComplete();
AddSliderStep("scale", 0.1, 2, 1, scale =>
{
if (logo != null)
Child.Scale = new Vector2(this.scale = (float)scale);
});
}
[Test]
public void TestBasic()
{
AddStep("Add logo", () =>
{
Child = new OsuLogo
Child = logo = new OsuLogo
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(scale),
};
});
}
[Test]
public void TestChristmas()
{
AddStep("Add logo", () =>
{
Child = logo = new OsuLogoChristmas
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(scale),
};
});
}

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays.Volume;
@ -59,13 +60,12 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestAltScrollNotBlocked()
{
bool scrollReceived = false;
TestGlobalScrollAdjustsVolume volumeAdjust = null!;
AddStep("add volume control receptor", () => Add(new VolumeControlReceptor
AddStep("add volume control receptor", () => Add(volumeAdjust = new TestGlobalScrollAdjustsVolume
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue,
ScrollActionRequested = (_, _, _) => scrollReceived = true,
}));
AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
@ -75,10 +75,21 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.ScrollVerticalBy(10);
});
AddAssert("receptor received scroll input", () => scrollReceived);
AddAssert("receptor received scroll input", () => volumeAdjust.ScrollReceived);
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
}
public partial class TestGlobalScrollAdjustsVolume : GlobalScrollAdjustsVolume
{
public bool ScrollReceived { get; private set; }
protected override bool OnScroll(ScrollEvent e)
{
ScrollReceived = true;
return base.OnScroll(e);
}
}
private partial class TestOverlay : OsuFocusedOverlayContainer
{
[BackgroundDependencyLoader]

View File

@ -1,8 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Overlays;
using osu.Game.Overlays.Volume;
@ -11,7 +10,14 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneVolumeOverlay : OsuTestScene
{
private VolumeOverlay volume;
private VolumeOverlay volume = null!;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs(volume = new VolumeOverlay());
return dependencies;
}
protected override void LoadComplete()
{
@ -19,12 +25,10 @@ namespace osu.Game.Tests.Visual.UserInterface
AddRange(new Drawable[]
{
volume = new VolumeOverlay(),
new VolumeControlReceptor
volume,
new GlobalScrollAdjustsVolume
{
RelativeSizeAxes = Axes.Both,
ActionRequested = action => volume.Adjust(action),
ScrollActionRequested = (action, amount, isPrecise) => volume.Adjust(action, amount, isPrecise),
},
});

View File

@ -152,6 +152,12 @@ namespace osu.Game.Tournament.Tests.Components
AddStep("change channel to 2", () => chatDisplay.Channel.Value = testChannel2);
AddStep("change channel to 1", () => chatDisplay.Channel.Value = testChannel);
AddStep("!mp message (shouldn't display)", () => testChannel.AddNewMessages(new Message(nextMessageId())
{
Sender = redUser.ToAPIUser(),
Content = "!mp wangs"
}));
}
private int messageId;

View File

@ -15,6 +15,7 @@ using osu.Game.Graphics;
using osu.Game.Models;
using osu.Game.Rulesets;
using osu.Game.Screens.Menu;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
@ -207,7 +208,7 @@ namespace osu.Game.Tournament.Components
Children = new Drawable[]
{
new DiffPiece(stats),
new DiffPiece(("Star Rating", $"{beatmap.StarRating:0.00}{srExtra}"))
new DiffPiece(("Star Rating", $"{beatmap.StarRating.FormatStarRating()}{srExtra}"))
}
},
new FillFlowContainer

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -72,7 +73,13 @@ namespace osu.Game.Tournament.Components
public void Contract() => this.FadeOut(200);
protected override ChatLine CreateMessage(Message message) => new MatchMessage(message, ladderInfo);
protected override ChatLine? CreateMessage(Message message)
{
if (message.Content.StartsWith("!mp", StringComparison.Ordinal))
return null;
return new MatchMessage(message, ladderInfo);
}
protected override StandAloneDrawableChannel CreateDrawableChannel(Channel channel) => new MatchChannel(channel);

View File

@ -533,6 +533,16 @@ namespace osu.Game.Beatmaps
}
}
public void MarkPlayed(BeatmapInfo beatmapSetInfo) => Realm.Run(r =>
{
using var transaction = r.BeginWrite();
var beatmap = r.Find<BeatmapInfo>(beatmapSetInfo.ID)!;
beatmap.LastPlayed = DateTimeOffset.Now;
transaction.Commit();
});
#region Implementation of ICanAcceptFiles
public Task Import(params string[] paths) => beatmapImporter.Import(paths);

View File

@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
@ -23,10 +20,12 @@ namespace osu.Game.Beatmaps
/// </summary>
public partial class DifficultyRecommender : Component
{
public event Action? StarRatingUpdated;
private readonly LocalUserStatisticsProvider statisticsProvider;
[Resolved]
private Bindable<RulesetInfo> gameRuleset { get; set; }
private Bindable<RulesetInfo> gameRuleset { get; set; } = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
@ -78,10 +77,18 @@ namespace osu.Game.Beatmaps
private void updateMapping(RulesetInfo ruleset, UserStatistics statistics)
{
// algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
recommendedDifficultyMapping[ruleset.ShortName] = Math.Pow((double)(statistics.PP ?? 0), 0.4) * 0.195;
// algorithm taken from https://github.com/ppy/osu-web/blob/027026fccc91525e39cee5d2f369f1b343eb1bf1/app/Models/UserStatistics/Model.php#L93-L94
recommendedDifficultyMapping[ruleset.ShortName] =
ruleset.ShortName == @"taiko"
? Math.Pow((double)(statistics.PP ?? 0), 0.35) * 0.27
: Math.Pow((double)(statistics.PP ?? 0), 0.4) * 0.195;
StarRatingUpdated?.Invoke();
}
public double? GetRecommendedStarRatingFor(RulesetInfo ruleset)
=> recommendedDifficultyMapping.TryGetValue(ruleset.ShortName, out double starRating) ? starRating : null;
/// <summary>
/// Find the recommended difficulty from a selection of available difficulties for the current local user.
/// </summary>
@ -90,15 +97,14 @@ namespace osu.Game.Beatmaps
/// </remarks>
/// <param name="beatmaps">A collection of beatmaps to select a difficulty from.</param>
/// <returns>The recommended difficulty, or null if a recommendation could not be provided.</returns>
[CanBeNull]
public BeatmapInfo GetRecommendedBeatmap(IEnumerable<BeatmapInfo> beatmaps)
public BeatmapInfo? GetRecommendedBeatmap(IEnumerable<BeatmapInfo> beatmaps)
{
foreach (string r in orderedRulesets)
{
if (!recommendedDifficultyMapping.TryGetValue(r, out double recommendation))
continue;
BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.ShortName.Equals(r, StringComparison.Ordinal)).MinBy(b =>
BeatmapInfo? beatmapInfo = beatmaps.Where(b => b.Ruleset.ShortName.Equals(r, StringComparison.Ordinal)).MinBy(b =>
{
double difference = b.StarRating - recommendation;
return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder

View File

@ -5,7 +5,6 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -14,6 +13,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
@ -156,7 +156,7 @@ namespace osu.Game.Beatmaps.Drawables
displayedStars.BindValueChanged(s =>
{
starsText.Text = s.NewValue < 0 ? "-" : s.NewValue.ToLocalisableString("0.00");
starsText.Text = s.NewValue < 0 ? "-" : s.NewValue.FormatStarRating();
background.Colour = colours.ForStarDifficulty(s.NewValue);

View File

@ -13,6 +13,8 @@ namespace osu.Game.Beatmaps.Legacy
NewCombo = 1 << 2,
Spinner = 1 << 3,
ComboOffset = (1 << 4) | (1 << 5) | (1 << 6),
Hold = 1 << 7
Hold = 1 << 7,
ObjectTypes = Circle | Slider | Spinner | Hold
}
}

View File

@ -57,6 +57,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f, 0.01f);
SetDefault(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal);
SetDefault(OsuSetting.BeatmapListingFeaturedArtistFilter, true);
SetDefault(OsuSetting.ProfileCoverExpanded, true);
@ -202,6 +203,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.HideCountryFlags, false);
SetDefault(OsuSetting.MultiplayerRoomFilter, RoomPermissionsFilter.All);
SetDefault(OsuSetting.MultiplayerShowInProgressFilter, true);
SetDefault(OsuSetting.LastProcessedMetadataId, -1);
@ -447,6 +449,8 @@ namespace osu.Game.Configuration
EditorRotationOrigin,
EditorTimelineShowBreaks,
EditorAdjustExistingObjectsOnTimingChanges,
AlwaysRequireHoldingForPause
AlwaysRequireHoldingForPause,
MultiplayerShowInProgressFilter,
BeatmapListingFeaturedArtistFilter,
}
}

View File

@ -95,8 +95,9 @@ namespace osu.Game.Database
/// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction
/// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user.
/// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm.
/// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user.
/// </summary>
private const int schema_version = 44;
private const int schema_version = 45;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -1205,6 +1206,22 @@ namespace osu.Game.Database
break;
}
case 45:
{
// Cycling beat snap divisors no longer requires holding shift (just control).
var keyBindings = migration.NewRealm.All<RealmKeyBinding>();
var nextBeatSnapBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.EditorCycleNextBeatSnapDivisor);
if (nextBeatSnapBinding != null && nextBeatSnapBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Shift, InputKey.Control, InputKey.MouseWheelLeft }))
migration.NewRealm.Remove(nextBeatSnapBinding);
var previousBeatSnapBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.EditorCyclePreviousBeatSnapDivisor);
if (previousBeatSnapBinding != null && previousBeatSnapBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Shift, InputKey.Control, InputKey.MouseWheelRight }))
migration.NewRealm.Remove(previousBeatSnapBinding);
break;
}
}
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");

View File

@ -28,7 +28,6 @@ namespace osu.Game.Graphics.Backgrounds
[Resolved]
private IAPIProvider api { get; set; }
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private Bindable<SeasonalBackgroundMode> seasonalBackgroundMode;
private Bindable<APISeasonalBackgrounds> seasonalBackgrounds;
@ -47,13 +46,12 @@ namespace osu.Game.Graphics.Backgrounds
SeasonalBackgroundChanged?.Invoke();
});
apiState.BindTo(api.State);
apiState.BindValueChanged(fetchSeasonalBackgrounds, true);
fetchSeasonalBackgrounds();
}
private void fetchSeasonalBackgrounds(ValueChangedEvent<APIState> stateChanged)
private void fetchSeasonalBackgrounds()
{
if (seasonalBackgrounds.Value != null || stateChanged.NewValue != APIState.Online)
if (seasonalBackgrounds.Value != null)
return;
var request = new GetSeasonalBackgroundsRequest();

View File

@ -195,6 +195,27 @@ namespace osu.Game.Graphics
}
}
/// <summary>
/// Retrieves the accent colour representing a <see cref="Room"/>'s current status.
/// </summary>
public Color4 ForRoomStatus(Room room)
{
if (room.HasEnded)
return YellowDarker;
switch (room.Status)
{
case RoomStatus.Playing:
return Purple;
default:
if (room.HasPassword)
return GreenDark;
return GreenLight;
}
}
/// <summary>
/// Retrieves colour for a <see cref="RankingTier"/>.
/// See https://www.figma.com/file/YHWhp9wZ089YXgB7pe6L1k/Tier-Colours

View File

@ -62,8 +62,12 @@ namespace osu.Game.IO.Serialization.Converters
if (tok["$type"] == null)
throw new JsonException("Expected $type token.");
string typeName = lookupTable[(int)tok["$type"]];
var instance = (T)Activator.CreateInstance(Type.GetType(typeName).AsNonNull())!;
// Prevent instantiation of types that do not inherit the type targetted by this converter
Type type = Type.GetType(lookupTable[(int)tok["$type"]]).AsNonNull();
if (!type.IsAssignableTo(typeof(T)))
continue;
var instance = (T)Activator.CreateInstance(type)!;
serializer.Populate(itemReader, instance);
list.Add(instance);

View File

@ -142,10 +142,8 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.EditorDecreaseDistanceSpacing),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing),
// Framework automatically converts wheel up/down to left/right when shift is held.
// See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/StateChanges/MouseScrollRelativeInput.cs#L37-L38.
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCycleNextBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl),
new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject),

View File

@ -28,6 +28,11 @@ This includes content that may not be correctly licensed for osu! usage. Browse
/// </summary>
public static LocalisableString UserContentConfirmButtonText => new TranslatableString(getKey(@"understood"), @"I understand");
/// <summary>
/// "Featured Artists are music artists who have collaborated with osu! to make a selection of their tracks available for use in beatmaps. For some osu! releases, we showcase only featured artist beatmaps to better support the surrounding ecosystem."
/// </summary>
public static LocalisableString FeaturedArtistsTooltip => new TranslatableString(getKey(@"featured_artists_disabled_tooltip"), @"Featured Artists are music artists who have collaborated with osu! to make a selection of their tracks available for use in beatmaps. For some osu! releases, we showcase only featured artist beatmaps to better support the surrounding ecosystem.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -119,6 +119,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString AutoplayBeatmapShortcut => new TranslatableString(getKey(@"autoplay_beatmap_shortcut"), @"Ctrl-Enter at song select will start a beatmap in autoplay mode!");
/// <summary>
/// "&quot;Lazer&quot; is not an English word. The correct spelling for the bright light is &quot;laser&quot;."
/// </summary>
public static LocalisableString LazerIsNotAWord => new TranslatableString(getKey(@"lazer_is_not_a_word"), @"""Lazer"" is not an English word. The correct spelling for the bright light is ""laser"".");
/// <summary>
/// "Multithreading support means that even with low &quot;FPS&quot; your input and judgements will be accurate!"
/// </summary>

View File

@ -0,0 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class RoomStatusPillStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.RoomStatusPill";
/// <summary>
/// "Ended"
/// </summary>
public static LocalisableString Ended => new TranslatableString(getKey(@"ended"), @"Ended");
/// <summary>
/// "Playing"
/// </summary>
public static LocalisableString Playing => new TranslatableString(getKey(@"playing"), @"Playing");
/// <summary>
/// "Open (Private)"
/// </summary>
public static LocalisableString OpenPrivate => new TranslatableString(getKey(@"open_private"), @"Open (Private)");
/// <summary>
/// "Open"
/// </summary>
public static LocalisableString Open => new TranslatableString(getKey(@"open"), @"Open");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -0,0 +1,49 @@
// 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.Localisation;
namespace osu.Game.Localisation.SkinComponents
{
public static class SkinnableModDisplayStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.SkinnableModDisplay";
/// <summary>
/// "Show extended information"
/// </summary>
public static LocalisableString ShowExtendedInformation => new TranslatableString(getKey(@"show_extended_information"), @"Show extended information");
/// <summary>
/// "Whether to show extended information for each mod."
/// </summary>
public static LocalisableString ShowExtendedInformationDescription => new TranslatableString(getKey(@"whether_to_show_extended_information"), @"Whether to show extended information for each mod.");
/// <summary>
/// "Expansion mode"
/// </summary>
public static LocalisableString ExpansionMode => new TranslatableString(getKey(@"expansion_mode"), @"Expansion mode");
/// <summary>
/// "How the mod display expands when interacted with."
/// </summary>
public static LocalisableString ExpansionModeDescription => new TranslatableString(getKey(@"how_the_mod_display_expands"), @"How the mod display expands when interacted with.");
/// <summary>
/// "Expand on hover"
/// </summary>
public static LocalisableString ExpandOnHover => new TranslatableString(getKey(@"expand_on_hover"), @"Expand on hover");
/// <summary>
/// "Always contracted"
/// </summary>
public static LocalisableString AlwaysContracted => new TranslatableString(getKey(@"always_contracted"), @"Always contracted");
/// <summary>
/// "Always expanded"
/// </summary>
public static LocalisableString AlwaysExpanded => new TranslatableString(getKey(@"always_expanded"), @"Always expanded");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -54,16 +54,6 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString BeatmapHitsounds => new TranslatableString(getKey(@"beatmap_hitsounds"), @"Beatmap hitsounds");
/// <summary>
/// "Export selected skin"
/// </summary>
public static LocalisableString ExportSkinButton => new TranslatableString(getKey(@"export_skin_button"), @"Export selected skin");
/// <summary>
/// "Delete selected skin"
/// </summary>
public static LocalisableString DeleteSkinButton => new TranslatableString(getKey(@"delete_skin_button"), @"Delete selected skin");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -1,9 +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.
#nullable disable
using System;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -21,18 +20,18 @@ using osuTK.Input;
namespace osu.Game.Online.Chat
{
/// <summary>
/// Display a chat channel in an insolated region.
/// Display a chat channel in an isolated region.
/// </summary>
public partial class StandAloneChatDisplay : CompositeDrawable
{
[Cached]
public readonly Bindable<Channel> Channel = new Bindable<Channel>();
public readonly Bindable<Channel?> Channel = new Bindable<Channel?>();
protected readonly ChatTextBox TextBox;
protected readonly ChatTextBox? TextBox;
private ChannelManager channelManager;
private ChannelManager? channelManager;
private StandAloneDrawableChannel drawableChannel;
private StandAloneDrawableChannel? drawableChannel;
private readonly bool postingTextBox;
@ -93,6 +92,8 @@ namespace osu.Game.Online.Chat
private void postMessage(TextBox sender, bool newText)
{
Debug.Assert(TextBox != null);
string text = TextBox.Text.Trim();
if (string.IsNullOrWhiteSpace(text))
@ -106,9 +107,9 @@ namespace osu.Game.Online.Chat
TextBox.Text = string.Empty;
}
protected virtual ChatLine CreateMessage(Message message) => new StandAloneMessage(message);
protected virtual ChatLine? CreateMessage(Message message) => new StandAloneMessage(message);
private void channelChanged(ValueChangedEvent<Channel> e)
private void channelChanged(ValueChangedEvent<Channel?> e)
{
drawableChannel?.Expire();
@ -128,8 +129,8 @@ namespace osu.Game.Online.Chat
public partial class ChatTextBox : HistoryTextBox
{
public Action Focus;
public Action FocusLost;
public Action? Focus;
public Action? FocusLost;
protected override bool OnKeyDown(KeyDownEvent e)
{
@ -171,14 +172,14 @@ namespace osu.Game.Online.Chat
public partial class StandAloneDrawableChannel : DrawableChannel
{
public Func<Message, ChatLine> CreateChatLineAction;
public Func<Message, ChatLine?>? CreateChatLineAction;
public StandAloneDrawableChannel(Channel channel)
: base(channel)
{
}
protected override ChatLine CreateChatLine(Message m) => CreateChatLineAction(m);
protected override ChatLine? CreateChatLine(Message m) => CreateChatLineAction?.Invoke(m) ?? null;
protected override DaySeparator CreateDaySeparator(DateTimeOffset time) => new StandAloneDaySeparator(time);
}

View File

@ -18,7 +18,6 @@ using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@ -395,15 +394,17 @@ namespace osu.Game.Online.Multiplayer
switch (state)
{
case MultiplayerRoomState.Open:
APIRoom.Status = APIRoom.HasPassword ? new RoomStatusOpenPrivate() : new RoomStatusOpen();
APIRoom.Status = RoomStatus.Idle;
break;
case MultiplayerRoomState.WaitingForLoad:
case MultiplayerRoomState.Playing:
APIRoom.Status = new RoomStatusPlaying();
APIRoom.Status = RoomStatus.Playing;
break;
case MultiplayerRoomState.Closed:
APIRoom.Status = new RoomStatusEnded();
APIRoom.EndDate = DateTimeOffset.Now;
APIRoom.Status = RoomStatus.Idle;
break;
}
@ -821,7 +822,6 @@ namespace osu.Game.Online.Multiplayer
Room.Settings = settings;
APIRoom.Name = Room.Settings.Name;
APIRoom.Password = Room.Settings.Password;
APIRoom.Status = string.IsNullOrEmpty(Room.Settings.Password) ? new RoomStatusOpen() : new RoomStatusOpenPrivate();
APIRoom.Type = Room.Settings.MatchType;
APIRoom.QueueMode = Room.Settings.QueueMode;
APIRoom.AutoStartDuration = Room.Settings.AutoStartDuration;

View File

@ -11,28 +11,33 @@ namespace osu.Game.Online.Rooms
{
public class GetRoomsRequest : APIRequest<List<Room>>
{
private readonly RoomStatusFilter status;
private readonly RoomModeFilter mode;
private readonly RoomStatusFilter? status;
private readonly string category;
public GetRoomsRequest(RoomStatusFilter status, string category)
public GetRoomsRequest(FilterCriteria filterCriteria)
{
this.status = status;
this.category = category;
mode = filterCriteria.Mode;
category = filterCriteria.Category;
status = filterCriteria.Status;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
if (status != RoomStatusFilter.Open)
req.AddParameter("mode", status.ToString().ToSnakeCase().ToLowerInvariant());
if (mode != RoomModeFilter.Open)
req.AddParameter(@"mode", mode.ToString().ToSnakeCase().ToLowerInvariant());
if (status != null)
req.AddParameter(@"status", status.Value.ToString().ToSnakeCase().ToLowerInvariant());
if (!string.IsNullOrEmpty(category))
req.AddParameter("category", category);
req.AddParameter(@"category", category);
return req;
}
protected override string Target => "rooms";
protected override string Target => @"rooms";
}
}

View File

@ -6,12 +6,10 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using osu.Game.IO.Serialization.Converters;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms.RoomStatuses;
namespace osu.Game.Online.Rooms
{
@ -248,7 +246,7 @@ namespace osu.Game.Online.Rooms
}
/// <summary>
/// The current room status.
/// The current status of the room.
/// </summary>
public RoomStatus Status
{
@ -265,18 +263,6 @@ namespace osu.Game.Online.Rooms
set => SetField(ref availability, value);
}
[OnDeserialized]
private void onDeserialised(StreamingContext context)
{
// API doesn't populate status so let's do it here.
if (EndDate != null && DateTimeOffset.Now >= EndDate)
Status = new RoomStatusEnded();
else if (HasPassword)
Status = new RoomStatusOpenPrivate();
else
Status = new RoomStatusOpen();
}
[JsonProperty("id")]
private long? roomId;
@ -349,8 +335,9 @@ namespace osu.Game.Online.Rooms
[JsonProperty("channel_id")]
private int channelId;
// Not serialised (see: GetRoomsRequest).
private RoomStatus status = new RoomStatusOpen();
[JsonProperty("status")]
[JsonConverter(typeof(SnakeCaseStringEnumConverter))]
private RoomStatus status;
// Not yet serialised (not implemented).
private RoomAvailability availability;
@ -388,6 +375,15 @@ namespace osu.Game.Online.Rooms
RecentParticipants = other.RecentParticipants;
}
/// <summary>
/// Whether the room is no longer available.
/// </summary>
/// <remarks>
/// This property does not update in real-time and needs to be queried periodically.
/// Subscribe to <see cref="EndDate"/> to be notified of any immediate changes.
/// </remarks>
public bool HasEnded => DateTimeOffset.Now >= EndDate;
[JsonObject(MemberSerialization.OptIn)]
public class RoomPlaylistItemStats
{

View File

@ -1,19 +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.
#nullable disable
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Online.Rooms
{
public abstract class RoomStatus
public enum RoomStatus
{
public abstract string Message { get; }
public abstract Color4 GetAppropriateColour(OsuColour colours);
public override int GetHashCode() => GetType().GetHashCode();
public override bool Equals(object obj) => GetType() == obj?.GetType();
Idle,
Playing,
}
}

View File

@ -1,14 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Online.Rooms.RoomStatuses
{
public class RoomStatusEnded : RoomStatus
{
public override string Message => "Ended";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.YellowDarker;
}
}

View File

@ -1,14 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Online.Rooms.RoomStatuses
{
public class RoomStatusOpen : RoomStatus
{
public override string Message => "Open";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenLight;
}
}

View File

@ -1,14 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Online.Rooms.RoomStatuses
{
public class RoomStatusOpenPrivate : RoomStatus
{
public override string Message => "Open (Private)";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDark;
}
}

View File

@ -1,14 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Online.Rooms.RoomStatuses
{
public class RoomStatusPlaying : RoomStatus
{
public override string Message => "Playing";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.Purple;
}
}

View File

@ -57,7 +57,6 @@ using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.OSD;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Overlays.Toolbar;
using osu.Game.Overlays.Volume;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens;
@ -69,6 +68,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Seasonal;
using osu.Game.Skinning;
using osu.Game.Updater;
using osu.Game.Users;
@ -221,6 +221,11 @@ namespace osu.Game
private readonly List<OverlayContainer> visibleBlockingOverlays = new List<OverlayContainer>();
/// <summary>
/// Whether the game should be limited to only display officially licensed content.
/// </summary>
public virtual bool HideUnlicensedContent => false;
public OsuGame(string[] args = null)
{
this.args = args;
@ -320,6 +325,7 @@ namespace osu.Game
if (host.Window != null)
{
host.Window.CursorState |= CursorState.Hidden;
host.Window.DragDrop += path =>
{
// on macOS/iOS, URL associations are handled via SDL_DROPFILE events.
@ -362,7 +368,10 @@ namespace osu.Game
{
SentryLogger.AttachUser(API.LocalUser);
dependencies.Cache(osuLogo = new OsuLogo { Alpha = 0 });
if (SeasonalUIConfig.ENABLED)
dependencies.CacheAs(osuLogo = new OsuLogoChristmas { Alpha = 0 });
else
dependencies.CacheAs(osuLogo = new OsuLogo { Alpha = 0 });
// bind config int to database RulesetInfo
configRuleset = LocalConfig.GetBindable<string>(OsuSetting.Ruleset);
@ -980,12 +989,6 @@ namespace osu.Game
AddRange(new Drawable[]
{
new VolumeControlReceptor
{
RelativeSizeAxes = Axes.Both,
ActionRequested = action => volume.Adjust(action),
ScrollActionRequested = (action, amount, isPrecise) => volume.Adjust(action, amount, isPrecise),
},
ScreenOffsetContainer = new Container
{
RelativeSizeAxes = Axes.Both,
@ -1432,6 +1435,19 @@ namespace osu.Game
switch (e.Action)
{
case GlobalAction.DecreaseVolume:
case GlobalAction.IncreaseVolume:
return volume.Adjust(e.Action);
case GlobalAction.ToggleMute:
case GlobalAction.NextVolumeMeter:
case GlobalAction.PreviousVolumeMeter:
if (e.Repeat)
return true;
return volume.Adjust(e.Action);
case GlobalAction.ToggleFPSDisplay:
fpsCounter.ToggleVisibility();
return true;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -29,7 +27,7 @@ namespace osu.Game.Overlays.BeatmapListing
/// <summary>
/// Any time the text box receives key events (even while masked).
/// </summary>
public Action TypingStarted;
public Action? TypingStarted;
public Bindable<string> Query => textBox.Current;
@ -51,7 +49,7 @@ namespace osu.Game.Overlays.BeatmapListing
public Bindable<SearchExplicit> ExplicitContent => explicitContentFilter.Current;
public APIBeatmapSet BeatmapSet
public APIBeatmapSet? BeatmapSet
{
set
{
@ -67,7 +65,7 @@ namespace osu.Game.Overlays.BeatmapListing
}
private readonly BeatmapSearchTextBox textBox;
private readonly BeatmapSearchMultipleSelectionFilterRow<SearchGeneral> generalFilter;
private readonly BeatmapSearchGeneralFilterRow generalFilter;
private readonly BeatmapSearchRulesetFilterRow modeFilter;
private readonly BeatmapSearchFilterRow<SearchCategory> categoryFilter;
private readonly BeatmapSearchFilterRow<SearchGenre> genreFilter;
@ -151,7 +149,7 @@ namespace osu.Game.Overlays.BeatmapListing
categoryFilter.Current.Value = SearchCategory.Leaderboard;
}
private IBindable<bool> allowExplicitContent;
private IBindable<bool> allowExplicitContent = null!;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuConfigManager config)
@ -165,6 +163,13 @@ namespace osu.Game.Overlays.BeatmapListing
}, true);
}
protected override void LoadComplete()
{
base.LoadComplete();
generalFilter.Ruleset.BindTo(Ruleset);
}
public void TakeFocus() => textBox.TakeFocus();
private partial class BeatmapSearchTextBox : BasicSearchTextBox
@ -172,7 +177,7 @@ namespace osu.Game.Overlays.BeatmapListing
/// <summary>
/// Any time the text box receives key events (even while masked).
/// </summary>
public Action TextChanged;
public Action? TextChanged;
protected override Color4 SelectionColour => Color4.Gray;

View File

@ -1,18 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Overlays.Dialog;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Utils;
using osuTK.Graphics;
using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
@ -20,27 +26,97 @@ namespace osu.Game.Overlays.BeatmapListing
{
public partial class BeatmapSearchGeneralFilterRow : BeatmapSearchMultipleSelectionFilterRow<SearchGeneral>
{
public readonly IBindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
public BeatmapSearchGeneralFilterRow()
: base(BeatmapsStrings.ListingSearchFiltersGeneral)
{
}
protected override MultipleSelectionFilter CreateMultipleSelectionFilter() => new GeneralFilter();
protected override MultipleSelectionFilter CreateMultipleSelectionFilter() => new GeneralFilter
{
Ruleset = { BindTarget = Ruleset }
};
private partial class GeneralFilter : MultipleSelectionFilter
{
public readonly IBindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
protected override MultipleSelectionFilterTabItem CreateTabItem(SearchGeneral value)
{
if (value == SearchGeneral.FeaturedArtists)
return new FeaturedArtistsTabItem();
switch (value)
{
case SearchGeneral.Recommended:
return new RecommendedDifficultyTabItem
{
Ruleset = { BindTarget = Ruleset }
};
return new MultipleSelectionFilterTabItem(value);
case SearchGeneral.FeaturedArtists:
return new FeaturedArtistsTabItem();
default:
return new MultipleSelectionFilterTabItem(value);
}
}
}
private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem
private partial class RecommendedDifficultyTabItem : MultipleSelectionFilterTabItem
{
private Bindable<bool> disclaimerShown;
public readonly IBindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
[Resolved]
private DifficultyRecommender? recommender { get; set; }
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
public RecommendedDifficultyTabItem()
: base(SearchGeneral.Recommended)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
if (recommender != null)
recommender.StarRatingUpdated += updateText;
Ruleset.BindValueChanged(_ => updateText(), true);
}
private void updateText()
{
// fallback to profile default game mode if beatmap listing mode filter is set to Any
// TODO: find a way to update `PlayMode` when the profile default game mode has changed
RulesetInfo? ruleset = Ruleset.Value.IsLegacyRuleset() ? Ruleset.Value : rulesets.GetRuleset(api.LocalUser.Value.PlayMode);
if (ruleset == null) return;
double? starRating = recommender?.GetRecommendedStarRatingFor(ruleset);
if (starRating != null)
Text.Text = LocalisableString.Interpolate($"{Value.GetLocalisableDescription()} ({starRating.Value.FormatStarRating()})");
else
Text.Text = Value.GetLocalisableDescription();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (recommender != null)
recommender.StarRatingUpdated -= updateText;
}
}
private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem, IHasTooltip
{
private Bindable<bool> disclaimerShown = null!;
public FeaturedArtistsTabItem()
: base(SearchGeneral.FeaturedArtists)
@ -48,19 +124,38 @@ namespace osu.Game.Overlays.BeatmapListing
}
[Resolved]
private OsuColour colours { get; set; }
private OsuColour colours { get; set; } = null!;
[Resolved]
private SessionStatics sessionStatics { get; set; }
private OsuConfigManager config { get; set; } = null!;
[Resolved(canBeNull: true)]
private IDialogOverlay dialogOverlay { get; set; }
[Resolved]
private SessionStatics sessionStatics { get; set; } = null!;
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
[Resolved]
private OsuGame? game { get; set; }
public LocalisableString TooltipText => BeatmapOverlayStrings.FeaturedArtistsTooltip;
protected override void LoadComplete()
{
base.LoadComplete();
config.BindWith(OsuSetting.BeatmapListingFeaturedArtistFilter, Active);
disclaimerShown = sessionStatics.GetBindable<bool>(Static.FeaturedArtistDisclaimerShownOnce);
// no need to show the disclaimer if the user already had it toggled off in config.
if (!Active.Value)
disclaimerShown.Value = true;
if (game?.HideUnlicensedContent == true)
{
Enabled.Value = false;
Active.Disabled = true;
}
}
protected override Color4 ColourNormal => colours.Orange1;
@ -68,6 +163,9 @@ namespace osu.Game.Overlays.BeatmapListing
protected override bool OnClick(ClickEvent e)
{
if (!Enabled.Value)
return true;
if (!disclaimerShown.Value && dialogOverlay != null)
{
dialogOverlay.Push(new FeaturedArtistConfirmDialog(() =>

View File

@ -73,7 +73,10 @@ namespace osu.Game.Overlays.BeatmapListing
private void currentChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
foreach (var c in Children)
c.Active.Value = Current.Contains(c.Value);
{
if (!c.Active.Disabled)
c.Active.Value = Current.Contains(c.Value);
}
}
/// <summary>
@ -100,7 +103,7 @@ namespace osu.Game.Overlays.BeatmapListing
protected partial class MultipleSelectionFilterTabItem : FilterTabItem<T>
{
private Drawable activeContent = null!;
private Container activeContent = null!;
private Circle background = null!;
public MultipleSelectionFilterTabItem(T value)
@ -160,7 +163,9 @@ namespace osu.Game.Overlays.BeatmapListing
{
Color4 colour = Active.Value ? ColourActive : ColourNormal;
if (IsHovered)
if (!Enabled.Value)
colour = colour.Darken(1f);
else if (IsHovered)
colour = Active.Value ? colour.Darken(0.2f) : colour.Lighten(0.2f);
if (Active.Value)

View File

@ -57,7 +57,9 @@ namespace osu.Game.Overlays.BeatmapListing
{
base.LoadComplete();
Enabled.BindValueChanged(_ => UpdateState());
UpdateState();
FinishTransforms(true);
}

View File

@ -21,6 +21,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Overlays.BeatmapSet
@ -185,7 +186,7 @@ namespace osu.Game.Overlays.BeatmapSet
OnHovered = beatmap =>
{
showBeatmap(beatmap);
starRating.Text = beatmap.StarRating.ToLocalisableString(@"0.00");
starRating.Text = beatmap.StarRating.FormatStarRating();
starRatingContainer.FadeIn(100);
},
OnClicked = beatmap => { Beatmap.Value = beatmap; },

View File

@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -132,6 +133,7 @@ namespace osu.Game.Overlays.Chat
Channel.PendingMessageResolved -= pendingMessageResolved;
}
[CanBeNull]
protected virtual ChatLine CreateChatLine(Message m) => new ChatLine(m);
protected virtual DaySeparator CreateDaySeparator(DateTimeOffset time) => new DaySeparator(time);
@ -155,8 +157,13 @@ namespace osu.Game.Overlays.Chat
{
addDaySeparatorIfRequired(lastMessage, message);
ChatLineFlow.Add(CreateChatLine(message));
lastMessage = message;
var chatLine = CreateChatLine(message);
if (chatLine != null)
{
ChatLineFlow.Add(chatLine);
lastMessage = message;
}
}
var staleMessages = chatLines.Where(c => c.LifetimeEnd == double.MaxValue).ToArray();

View File

@ -4,12 +4,14 @@
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
@ -162,18 +164,77 @@ namespace osu.Game.Overlays.Profile.Header.Components
scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0;
detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";
var rankHighest = user?.RankHighest;
detailGlobalRank.ContentTooltipText = rankHighest != null
? UsersStrings.ShowRankHighest(rankHighest.Rank.ToLocalisableString("\\##,##0"), rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy"))
: string.Empty;
detailGlobalRank.ContentTooltipText = getGlobalRankTooltipText(user);
detailCountryRank.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";
detailCountryRank.ContentTooltipText = getCountryRankTooltipText(user);
rankGraph.Statistics.Value = user?.Statistics;
}
private static LocalisableString getGlobalRankTooltipText(APIUser? user)
{
var rankHighest = user?.RankHighest;
var variants = user?.Statistics?.Variants;
LocalisableString? result = null;
if (variants?.Count > 0)
{
foreach (var variant in variants)
{
if (variant.GlobalRank != null)
{
var variantText = LocalisableString.Interpolate($"{variant.VariantType.GetLocalisableDescription()}: {variant.GlobalRank.ToLocalisableString("\\##,##0")}");
if (result == null)
result = variantText;
else
result = LocalisableString.Interpolate($"{result}\n{variantText}");
}
}
}
if (rankHighest != null)
{
var rankHighestText = UsersStrings.ShowRankHighest(
rankHighest.Rank.ToLocalisableString("\\##,##0"),
rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy"));
if (result == null)
result = rankHighestText;
else
result = LocalisableString.Interpolate($"{result}\n{rankHighestText}");
}
return result ?? default;
}
private static LocalisableString getCountryRankTooltipText(APIUser? user)
{
var variants = user?.Statistics?.Variants;
LocalisableString? result = null;
if (variants?.Count > 0)
{
foreach (var variant in variants)
{
if (variant.CountryRank != null)
{
var variantText = LocalisableString.Interpolate($"{variant.VariantType.GetLocalisableDescription()}: {variant.CountryRank.ToLocalisableString("\\##,##0")}");
if (result == null)
result = variantText;
else
result = LocalisableString.Interpolate($"{result}\n{variantText}");
}
}
}
return result ?? default;
}
private partial class ScoreRankInfo : CompositeDrawable
{
private readonly OsuSpriteText rankCount;

View File

@ -57,10 +57,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
LabelText = MouseSettingsStrings.HighPrecisionMouse,
TooltipText = MouseSettingsStrings.HighPrecisionMouseTooltip,
Current = relativeMode,
Keywords = new[] { @"raw", @"input", @"relative", @"cursor" }
Keywords = new[] { @"raw", @"input", @"relative", @"cursor", "sensitivity", "speed", "velocity" },
},
new SensitivitySetting
{
Keywords = new[] { "speed", "velocity" },
LabelText = MouseSettingsStrings.CursorSensitivity,
Current = localSensitivity
},

View File

@ -9,17 +9,23 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Screens.Select;
using osu.Game.Skinning;
using osuTK;
using Realms;
namespace osu.Game.Overlays.Settings.Sections
@ -64,13 +70,26 @@ namespace osu.Game.Overlays.Settings.Sections
Current = skins.CurrentSkinInfo,
Keywords = new[] { @"skins" },
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS },
Children = new Drawable[]
{
// This is all super-temporary until we move skin settings to their own panel / overlay.
new RenameSkinButton { Padding = new MarginPadding(), RelativeSizeAxes = Axes.None, Width = 120 },
new ExportSkinButton { Padding = new MarginPadding(), RelativeSizeAxes = Axes.None, Width = 120 },
new DeleteSkinButton { Padding = new MarginPadding(), RelativeSizeAxes = Axes.None, Width = 110 },
}
},
new SettingsButton
{
Text = SkinSettingsStrings.SkinLayoutEditor,
Action = () => skinEditor?.ToggleVisibility(),
},
new ExportSkinButton(),
new DeleteSkinButton(),
};
}
@ -136,6 +155,34 @@ namespace osu.Game.Overlays.Settings.Sections
}
}
public partial class RenameSkinButton : SettingsButton, IHasPopover
{
[Resolved]
private SkinManager skins { get; set; }
private Bindable<Skin> currentSkin;
[BackgroundDependencyLoader]
private void load()
{
Text = "Rename";
Action = this.ShowPopover;
}
protected override void LoadComplete()
{
base.LoadComplete();
currentSkin = skins.CurrentSkin.GetBoundCopy();
currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => !s.Protected), true);
}
public Popover GetPopover()
{
return new RenameSkinPopover();
}
}
public partial class ExportSkinButton : SettingsButton
{
[Resolved]
@ -146,7 +193,7 @@ namespace osu.Game.Overlays.Settings.Sections
[BackgroundDependencyLoader]
private void load()
{
Text = SkinSettingsStrings.ExportSkinButton;
Text = "Export";
Action = export;
}
@ -184,7 +231,7 @@ namespace osu.Game.Overlays.Settings.Sections
[BackgroundDependencyLoader]
private void load()
{
Text = SkinSettingsStrings.DeleteSkinButton;
Text = "Delete";
Action = delete;
}
@ -201,5 +248,63 @@ namespace osu.Game.Overlays.Settings.Sections
dialogOverlay?.Push(new SkinDeleteDialog(currentSkin.Value));
}
}
public partial class RenameSkinPopover : OsuPopover
{
[Resolved]
private SkinManager skins { get; set; }
private readonly FocusedTextBox textBox;
public RenameSkinPopover()
{
AutoSizeAxes = Axes.Both;
Origin = Anchor.TopCentre;
RoundedButton renameButton;
Child = new FillFlowContainer
{
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y,
Width = 250,
Spacing = new Vector2(10f),
Children = new Drawable[]
{
textBox = new FocusedTextBox
{
PlaceholderText = @"Skin name",
FontSize = OsuFont.DEFAULT_FONT_SIZE,
RelativeSizeAxes = Axes.X,
SelectAllOnFocus = true,
},
renameButton = new RoundedButton
{
Height = 40,
RelativeSizeAxes = Axes.X,
MatchingFilter = true,
Text = "Save",
}
}
};
renameButton.Action += rename;
textBox.OnCommit += (_, _) => rename();
}
protected override void PopIn()
{
textBox.Text = skins.CurrentSkinInfo.Value.Value.Name;
textBox.TakeFocus();
base.PopIn();
}
private void rename() => skins.CurrentSkinInfo.Value.PerformWrite(skin =>
{
skin.Name = textBox.Text;
PopOut();
});
}
}
}

View File

@ -36,11 +36,13 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
},
new SettingsCheckbox
{
Keywords = new[] { "intro", "welcome" },
LabelText = UserInterfaceStrings.InterfaceVoices,
Current = config.GetBindable<bool>(OsuSetting.MenuVoice)
},
new SettingsCheckbox
{
Keywords = new[] { "intro", "welcome" },
LabelText = UserInterfaceStrings.OsuMusicTheme,
Current = config.GetBindable<bool>(OsuSetting.MenuMusic)
},

View File

@ -0,0 +1,37 @@
// 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.Input.Events;
using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Volume
{
/// <summary>
/// Add to a container or screen to make scrolling anywhere in the container cause the global game volume to be adjusted.
/// </summary>
/// <remarks>
/// This is generally expected behaviour in many locations in osu!stable.
/// </remarks>
public partial class GlobalScrollAdjustsVolume : Container
{
[Resolved]
private VolumeOverlay? volumeOverlay { get; set; }
public GlobalScrollAdjustsVolume()
{
RelativeSizeAxes = Axes.Both;
}
protected override bool OnScroll(ScrollEvent e)
{
if (e.ScrollDelta.Y == 0)
return false;
// forward any unhandled mouse scroll events to the volume control.
return volumeOverlay?.Adjust(GlobalAction.IncreaseVolume, e.ScrollDelta.Y, e.IsPrecise) ?? false;
}
}
}

View File

@ -1,57 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Volume
{
public partial class VolumeControlReceptor : Container, IScrollBindingHandler<GlobalAction>, IHandleGlobalKeyboardInput
{
public Func<GlobalAction, bool> ActionRequested;
public Func<GlobalAction, float, bool, bool> ScrollActionRequested;
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
case GlobalAction.DecreaseVolume:
case GlobalAction.IncreaseVolume:
return ActionRequested?.Invoke(e.Action) == true;
case GlobalAction.ToggleMute:
case GlobalAction.NextVolumeMeter:
case GlobalAction.PreviousVolumeMeter:
if (!e.Repeat)
return ActionRequested?.Invoke(e.Action) == true;
return false;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
protected override bool OnScroll(ScrollEvent e)
{
if (e.ScrollDelta.Y == 0)
return false;
// forward any unhandled mouse scroll events to the volume control.
ScrollActionRequested?.Invoke(GlobalAction.IncreaseVolume, e.ScrollDelta.Y, e.IsPrecise);
return true;
}
public bool OnScroll(KeyBindingScrollEvent<GlobalAction> e) =>
ScrollActionRequested?.Invoke(e.Action, e.ScrollAmount, e.IsPrecise) ?? false;
}
}

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
public Vector2 Position { get; set; }
public LegacyHitObjectType LegacyType { get; set; }
public LegacyHitObjectType LegacyType { get; set; } = LegacyHitObjectType.Circle;
public override Judgement CreateJudgement() => new IgnoreJudgement();

View File

@ -1,6 +1,7 @@
// 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.Beatmaps.Legacy;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy
@ -16,5 +17,10 @@ namespace osu.Game.Rulesets.Objects.Legacy
public double Duration { get; set; }
public double EndTime => StartTime + Duration;
public ConvertHold()
{
LegacyType = LegacyHitObjectType.Hold;
}
}
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
namespace osu.Game.Rulesets.Objects.Legacy
{
@ -56,6 +57,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
public bool GenerateTicks { get; set; } = true;
public ConvertSlider()
{
LegacyType = LegacyHitObjectType.Slider;
}
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);

View File

@ -1,6 +1,7 @@
// 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.Beatmaps.Legacy;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy
@ -16,5 +17,10 @@ namespace osu.Game.Rulesets.Objects.Legacy
public double Duration { get; set; }
public double EndTime => StartTime + Duration;
public ConvertSpinner()
{
LegacyType = LegacyHitObjectType.Spinner;
}
}
}

View File

@ -39,7 +39,18 @@ namespace osu.Game.Rulesets.UI
private IMod mod;
private readonly bool showTooltip;
private readonly bool showExtendedInformation;
private bool showExtendedInformation;
public bool ShowExtendedInformation
{
get => showExtendedInformation;
set
{
showExtendedInformation = value;
updateExtendedInformation();
}
}
public IMod Mod
{

View File

@ -34,6 +34,9 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
[Resolved]
private IEditorChangeHandler? editorChangeHandler { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours, OverlayColourProvider colourProvider)
{
@ -110,6 +113,9 @@ namespace osu.Game.Screens.Edit.Timing
}
},
};
if (editorChangeHandler != null)
editorChangeHandler.OnStateChange += onUndoRedo;
}
protected override void LoadComplete()
@ -185,5 +191,21 @@ namespace osu.Game.Screens.Edit.Timing
selectedGroup.Value = group;
}
private void onUndoRedo()
{
// Best effort. We have no tracking of control points through undo/redo changes.
// If we don't deselect, things like offset changes could spawn groups to be added from previous states (see https://github.com/ppy/osu/issues/31098).
if (selectedGroup.Value != null && !Beatmap.ControlPointInfo.Groups.Contains(selectedGroup.Value))
selectedGroup.Value = null;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (editorChangeHandler != null)
editorChangeHandler.OnStateChange -= onUndoRedo;
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@ -21,6 +22,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Screens.Edit.Timing.RowAttributes;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Timing
{
@ -177,7 +179,7 @@ namespace osu.Game.Screens.Edit.Timing
private readonly BindableWithCurrent<ControlPointGroup> current = new BindableWithCurrent<ControlPointGroup>();
private Box background = null!;
private Box currentIndicator = null!;
private Drawable currentIndicator = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
@ -202,7 +204,7 @@ namespace osu.Game.Screens.Edit.Timing
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
InternalChildren = new[]
{
background = new Box
{
@ -210,11 +212,26 @@ namespace osu.Game.Screens.Edit.Timing
Colour = colourProvider.Background1,
Alpha = 0,
},
currentIndicator = new Box
currentIndicator = new Container
{
RelativeSizeAxes = Axes.Y,
Width = 5,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Y,
Width = 5,
},
new Box
{
RelativeSizeAxes = Axes.Y,
Blending = BlendingParameters.Additive,
X = 5,
Width = 150,
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.1f), Color4.White.Opacity(0))
},
}
},
new Container
{
@ -281,14 +298,8 @@ namespace osu.Game.Screens.Edit.Timing
bool hasCurrentTimingPoint = activeTimingPoint.Value != null && current.Value.ControlPoints.Contains(activeTimingPoint.Value);
bool hasCurrentEffectPoint = activeEffectPoint.Value != null && current.Value.ControlPoints.Contains(activeEffectPoint.Value);
if (IsHovered || isSelected)
background.FadeIn(100, Easing.OutQuint);
else if (hasCurrentTimingPoint || hasCurrentEffectPoint)
background.FadeTo(0.2f, 100, Easing.OutQuint);
else
background.FadeOut(100, Easing.OutQuint);
background.Colour = isSelected ? colourProvider.Colour3 : colourProvider.Background1;
background.FadeTo(IsHovered || isSelected ? 1 : 0, 100, Easing.OutQuint);
background.FadeColour(isSelected ? colourProvider.Colour3 : colourProvider.Background1, 100, Easing.OutQuint);
if (hasCurrentTimingPoint || hasCurrentEffectPoint)
{

View File

@ -6,6 +6,7 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shaders;
@ -15,6 +16,7 @@ using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Seasonal;
using IntroSequence = osu.Game.Configuration.IntroSequence;
namespace osu.Game.Screens
@ -37,6 +39,11 @@ namespace osu.Game.Screens
private IntroScreen getIntroSequence()
{
// Headless tests run too fast to load non-circles intros correctly.
// They will hit the "audio can't play" notification and cause random test failures.
if (SeasonalUIConfig.ENABLED && !DebugUtils.IsNUnitRunning)
return new IntroChristmas(createMainMenu);
if (introSequence == IntroSequence.Random)
introSequence = (IntroSequence)RNG.Next(0, (int)IntroSequence.Random);

View File

@ -24,6 +24,7 @@ using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.Volume;
using osu.Game.Rulesets;
using osu.Game.Screens.Backgrounds;
using osu.Game.Skinning;
@ -174,6 +175,8 @@ namespace osu.Game.Screens.Menu
return UsingThemedIntro = initialBeatmap != null;
}
AddInternal(new GlobalScrollAdjustsVolume());
}
public override void OnEntering(ScreenTransitionEvent e)
@ -207,7 +210,7 @@ namespace osu.Game.Screens.Menu
Text = NotificationsStrings.AudioPlaybackIssue
});
}
}, 5000);
}, 8000);
}
public override void OnResuming(ScreenTransitionEvent e)

View File

@ -28,6 +28,7 @@ using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Overlays.Volume;
using osu.Game.Rulesets;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
@ -35,6 +36,7 @@ using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Select;
using osu.Game.Seasonal;
using osuTK;
using osuTK.Graphics;
@ -124,6 +126,8 @@ namespace osu.Game.Screens.Menu
AddRangeInternal(new[]
{
SeasonalUIConfig.ENABLED ? new MainMenuSeasonalLighting() : Empty(),
new GlobalScrollAdjustsVolume(),
buttonsContainer = new ParallaxContainer
{
ParallaxAmount = 0.01f,
@ -159,14 +163,15 @@ namespace osu.Game.Screens.Menu
}
},
logoTarget = new Container { RelativeSizeAxes = Axes.Both, },
sideFlashes = new MenuSideFlashes(),
sideFlashes = SeasonalUIConfig.ENABLED ? new SeasonalMenuSideFlashes() : new MenuSideFlashes(),
songTicker = new SongTicker
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Margin = new MarginPadding { Right = 15, Top = 5 }
},
new KiaiMenuFountains(),
// For now, this is too much alongside the seasonal lighting.
SeasonalUIConfig.ENABLED ? Empty() : new KiaiMenuFountains(),
bottomElementsFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
@ -197,18 +202,20 @@ namespace osu.Game.Screens.Menu
holdToExitGameOverlay?.CreateProxy() ?? Empty()
});
float baseDim = SeasonalUIConfig.ENABLED ? 0.84f : 1;
Buttons.StateChanged += state =>
{
switch (state)
{
case ButtonSystemState.Initial:
case ButtonSystemState.Exit:
ApplyToBackground(b => b.FadeColour(Color4.White, 500, Easing.OutSine));
ApplyToBackground(b => b.FadeColour(OsuColour.Gray(baseDim), 500, Easing.OutSine));
onlineMenuBanner.State.Value = Visibility.Hidden;
break;
default:
ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.8f), 500, Easing.OutSine));
ApplyToBackground(b => b.FadeColour(OsuColour.Gray(baseDim * 0.8f), 500, Easing.OutSine));
onlineMenuBanner.State.Value = Visibility.Visible;
break;
}

View File

@ -1,21 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osuTK.Graphics;
using osu.Game.Skinning;
using osu.Game.Online.API;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
{
internal partial class MenuLogoVisualisation : LogoVisualisation
public partial class MenuLogoVisualisation : LogoVisualisation
{
private IBindable<APIUser> user;
private Bindable<Skin> skin;
private IBindable<APIUser> user = null!;
private Bindable<Skin> skin = null!;
[BackgroundDependencyLoader]
private void load(IAPIProvider api, SkinManager skinManager)
@ -23,11 +21,11 @@ namespace osu.Game.Screens.Menu
user = api.LocalUser.GetBoundCopy();
skin = skinManager.CurrentSkin.GetBoundCopy();
user.ValueChanged += _ => updateColour();
skin.BindValueChanged(_ => updateColour(), true);
user.ValueChanged += _ => UpdateColour();
skin.BindValueChanged(_ => UpdateColour(), true);
}
private void updateColour()
protected virtual void UpdateColour()
{
if (user.Value?.IsSupporter ?? false)
Colour = skin.Value.GetConfig<GlobalSkinColours, Color4>(GlobalSkinColours.MenuGlow)?.Value ?? Color4.White;

View File

@ -3,8 +3,10 @@
#nullable disable
using osuTK.Graphics;
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@ -13,17 +15,19 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Skinning;
using osu.Game.Online.API;
using System;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
{
public partial class MenuSideFlashes : BeatSyncedContainer
{
protected virtual bool RefreshColoursEveryFlash => false;
protected virtual float Intensity => 2;
private readonly IBindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
private Box leftBox;
@ -67,7 +71,7 @@ namespace osu.Game.Screens.Menu
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Y,
Width = box_width * 2,
Width = box_width * Intensity,
Height = 1.5f,
// align off-screen to make sure our edges don't become visible during parallax.
X = -box_width,
@ -79,7 +83,7 @@ namespace osu.Game.Screens.Menu
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Y,
Width = box_width * 2,
Width = box_width * Intensity,
Height = 1.5f,
X = box_width,
Alpha = 0,
@ -87,8 +91,11 @@ namespace osu.Game.Screens.Menu
}
};
user.ValueChanged += _ => updateColour();
skin.BindValueChanged(_ => updateColour(), true);
if (!RefreshColoursEveryFlash)
{
user.ValueChanged += _ => updateColour();
skin.BindValueChanged(_ => updateColour(), true);
}
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
@ -104,18 +111,28 @@ namespace osu.Game.Screens.Menu
private void flash(Drawable d, double beatLength, bool kiai, ChannelAmplitudes amplitudes)
{
d.FadeTo(Math.Max(0, ((ReferenceEquals(d, leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier)), box_fade_in_time)
if (RefreshColoursEveryFlash)
updateColour();
d.FadeTo(Math.Clamp(0.1f + ((ReferenceEquals(d, leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier), 0.1f, 1),
box_fade_in_time)
.Then()
.FadeOut(beatLength, Easing.In);
}
private void updateColour()
protected virtual Color4 GetBaseColour()
{
Color4 baseColour = colours.Blue;
if (user.Value?.IsSupporter ?? false)
baseColour = skin.Value.GetConfig<GlobalSkinColours, Color4>(GlobalSkinColours.MenuGlow)?.Value ?? baseColour;
return baseColour;
}
private void updateColour()
{
var baseColour = GetBaseColour();
// linear colour looks better in this case, so let's use it for now.
Color4 gradientDark = baseColour.Opacity(0).ToLinear();
Color4 gradientLight = baseColour.Opacity(0.6f).ToLinear();

View File

@ -122,7 +122,8 @@ namespace osu.Game.Screens.Menu
MenuTipStrings.RandomSkinShortcut,
MenuTipStrings.ToggleReplaySettingsShortcut,
MenuTipStrings.CopyModsFromScore,
MenuTipStrings.AutoplayBeatmapShortcut
MenuTipStrings.AutoplayBeatmapShortcut,
MenuTipStrings.LazerIsNotAWord
};
return tips[RNG.Next(0, tips.Length)];

View File

@ -53,8 +53,12 @@ namespace osu.Game.Screens.Menu
private Sample sampleClick;
private SampleChannel sampleClickChannel;
private Sample sampleBeat;
private Sample sampleDownbeat;
protected virtual MenuLogoVisualisation CreateMenuLogoVisualisation() => new MenuLogoVisualisation();
protected virtual double BeatSampleVariance => 0.1;
protected Sample SampleBeat;
protected Sample SampleDownbeat;
private readonly Container colourAndTriangles;
private readonly TrianglesV2 triangles;
@ -151,15 +155,15 @@ namespace osu.Game.Screens.Menu
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
visualizer = new MenuLogoVisualisation
visualizer = CreateMenuLogoVisualisation().With(v =>
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Alpha = visualizer_default_alpha,
Size = SCALE_ADJUST
},
new Container
v.RelativeSizeAxes = Axes.Both;
v.Origin = Anchor.Centre;
v.Anchor = Anchor.Centre;
v.Alpha = visualizer_default_alpha;
v.Size = SCALE_ADJUST;
}),
LogoElements = new Container
{
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
@ -243,6 +247,8 @@ namespace osu.Game.Screens.Menu
};
}
public Container LogoElements { get; private set; }
/// <summary>
/// Schedule a new external animation. Handled queueing and finishing previous animations in a sane way.
/// </summary>
@ -271,8 +277,9 @@ namespace osu.Game.Screens.Menu
private void load(TextureStore textures, AudioManager audio)
{
sampleClick = audio.Samples.Get(@"Menu/osu-logo-select");
sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat");
sampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat");
SampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat");
SampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat");
logo.Texture = textures.Get(@"Menu/logo");
ripple.Texture = textures.Get(@"Menu/logo");
@ -298,12 +305,13 @@ namespace osu.Game.Screens.Menu
{
if (beatIndex % timingPoint.TimeSignature.Numerator == 0)
{
sampleDownbeat?.Play();
SampleDownbeat?.Play();
}
else
{
var channel = sampleBeat.GetChannel();
channel.Frequency.Value = 0.95 + RNG.NextDouble(0.1);
var channel = SampleBeat.GetChannel();
channel.Frequency.Value = 1 - BeatSampleVariance / 2 + RNG.NextDouble(BeatSampleVariance);
channel.Play();
}
});

View File

@ -47,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
lastPollRequest?.Cancel();
var req = new GetRoomsRequest(Filter.Value.Status, Filter.Value.Category);
var req = new GetRoomsRequest(Filter.Value);
req.Success += result =>
{

View File

@ -29,18 +29,28 @@ namespace osu.Game.Screens.OnlinePlay.Components
base.LoadComplete();
room.PropertyChanged += onRoomPropertyChanged;
// Timed update required to track rooms which have hit the end time, see `HasEnded`.
Scheduler.AddDelayed(updateRoomStatus, 1000, true);
updateRoomStatus();
}
private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Room.Status))
updateRoomStatus();
switch (e.PropertyName)
{
case nameof(Room.Category):
case nameof(Room.Status):
case nameof(Room.EndDate):
case nameof(Room.HasPassword):
updateRoomStatus();
break;
}
}
private void updateRoomStatus()
{
this.FadeColour(colours.ForRoomCategory(room.Category) ?? room.Status.GetAppropriateColour(colours), transitionDuration);
this.FadeColour(colours.ForRoomCategory(room.Category) ?? colours.ForRoomStatus(room), transitionDuration);
}
protected override void Dispose(bool isDisposing)

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