1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-22 21:40:49 +08:00

Compare commits

..

118 Commits

91 changed files with 2723 additions and 527 deletions
+1 -1
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.
-1
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;
}
@@ -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();
}
}
@@ -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));
}
}
}
@@ -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();
}
}
@@ -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));
}
}
}
@@ -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
@@ -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
}
]
}
]
}
@@ -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:
@@ -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;
}
}
}
}
@@ -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;
@@ -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>
@@ -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;
}
}
}
@@ -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;
@@ -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);
@@ -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>();
@@ -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();
}
}
@@ -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));
}
}
}
@@ -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;
@@ -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,
@@ -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();
}
}
@@ -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));
}
}
}
+14
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();
}
}
@@ -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));
}
}
}
@@ -68,7 +68,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",
};
/// <summary>
@@ -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);
@@ -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();
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Game.Screens.Menu;
using osu.Game.Seasonal;
namespace osu.Game.Tests.Visual.Menus
{
@@ -6,8 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Screens.Menu;
using osu.Game.Seasonal;
namespace osu.Game.Tests.Visual.Menus
{
@@ -16,17 +15,15 @@ namespace osu.Game.Tests.Visual.Menus
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("prepare beatmap", () =>
{
var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == "7e26183e72a496f672c3a21292e6b469fdecd084d31c259ea10a31df5b46cd77");
var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == IntroChristmas.CHRISTMAS_BEATMAP_SET_HASH);
Beatmap.Value = beatmaps.GetWorkingBeatmap(setInfo!.Value.Beatmaps.First());
if (setInfo != null)
Beatmap.Value = beatmaps.GetWorkingBeatmap(setInfo.Value.Beatmaps.First());
});
AddStep("create lighting", () => Child = new MainMenuSeasonalLighting());
@@ -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);
}
}
}
@@ -4,6 +4,7 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.Menu;
using osu.Game.Seasonal;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
@@ -12,6 +13,19 @@ namespace osu.Game.Tests.Visual.UserInterface
{
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()
{
@@ -21,13 +35,22 @@ namespace osu.Game.Tests.Visual.UserInterface
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(scale),
};
});
}
AddSliderStep("scale", 0.1, 2, 1, scale =>
[Test]
public void TestChristmas()
{
AddStep("Add logo", () =>
{
if (logo != null)
Child.Scale = new Vector2((float)scale);
Child = logo = new OsuLogoChristmas
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(scale),
};
});
}
}
@@ -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]
@@ -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),
},
});
@@ -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;
@@ -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);
+10
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);
@@ -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
}
}
@@ -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);
@@ -450,5 +451,6 @@ namespace osu.Game.Configuration
EditorAdjustExistingObjectsOnTimingChanges,
AlwaysRequireHoldingForPause,
MultiplayerShowInProgressFilter,
BeatmapListingFeaturedArtistFilter,
}
}
+18 -1
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");
@@ -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();
@@ -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),
@@ -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}";
}
}
+5
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>
@@ -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}";
}
}
@@ -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}";
}
}
+14 -13
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);
}
+24 -8
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;
@@ -5,6 +5,7 @@ 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;
@@ -113,7 +114,7 @@ namespace osu.Game.Overlays.BeatmapListing
}
}
private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem
private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem, IHasTooltip
{
private Bindable<bool> disclaimerShown = null!;
@@ -125,17 +126,36 @@ namespace osu.Game.Overlays.BeatmapListing
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
[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;
@@ -143,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(() =>
@@ -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)
@@ -57,7 +57,9 @@ namespace osu.Game.Overlays.BeatmapListing
{
base.LoadComplete();
Enabled.BindValueChanged(_ => UpdateState());
UpdateState();
FinishTransforms(true);
}
+9 -2
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();
@@ -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();
});
}
}
}
@@ -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)
},
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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();
@@ -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;
}
}
}
@@ -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);
@@ -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;
}
}
}
+12 -1
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
{
+5 -1
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,7 +39,9 @@ namespace osu.Game.Screens
private IntroScreen getIntroSequence()
{
if (SeasonalUI.ENABLED)
// 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)
+3
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)
@@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
{
@@ -37,8 +36,6 @@ namespace osu.Game.Screens.Menu
X = -250,
},
};
Colour = SeasonalUI.ENABLED ? SeasonalUI.AMBIENT_COLOUR_2 : Color4.White;
}
private bool isTriggered;
+10 -5
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,7 +126,8 @@ namespace osu.Game.Screens.Menu
AddRangeInternal(new[]
{
SeasonalUI.ENABLED ? new MainMenuSeasonalLighting() : Empty(),
SeasonalUIConfig.ENABLED ? new MainMenuSeasonalLighting() : Empty(),
new GlobalScrollAdjustsVolume(),
buttonsContainer = new ParallaxContainer
{
ParallaxAmount = 0.01f,
@@ -160,7 +163,7 @@ 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,
@@ -168,7 +171,7 @@ namespace osu.Game.Screens.Menu
Margin = new MarginPadding { Right = 15, Top = 5 }
},
// For now, this is too much alongside the seasonal lighting.
SeasonalUI.ENABLED ? Empty() : new KiaiMenuFountains(),
SeasonalUIConfig.ENABLED ? Empty() : new KiaiMenuFountains(),
bottomElementsFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
@@ -199,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;
}
+7 -11
View File
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.API;
@@ -12,10 +10,10 @@ 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,15 +21,13 @@ 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 (SeasonalUI.ENABLED)
Colour = SeasonalUI.AMBIENT_COLOUR_1;
else if (user.Value?.IsSupporter ?? false)
if (user.Value?.IsSupporter ?? false)
Colour = skin.Value.GetConfig<GlobalSkinColours, Color4>(GlobalSkinColours.MenuGlow)?.Value ?? Color4.White;
else
Colour = Color4.White;
+20 -10
View File
@@ -11,7 +11,6 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
@@ -25,6 +24,10 @@ 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;
@@ -68,7 +71,7 @@ namespace osu.Game.Screens.Menu
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Y,
Width = box_width * (SeasonalUI.ENABLED ? 4 : 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,
@@ -80,7 +83,7 @@ namespace osu.Game.Screens.Menu
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Y,
Width = box_width * (SeasonalUI.ENABLED ? 4 : 2),
Width = box_width * Intensity,
Height = 1.5f,
X = box_width,
Alpha = 0,
@@ -88,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)
@@ -105,7 +111,7 @@ namespace osu.Game.Screens.Menu
private void flash(Drawable d, double beatLength, bool kiai, ChannelAmplitudes amplitudes)
{
if (SeasonalUI.ENABLED)
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),
@@ -114,15 +120,19 @@ namespace osu.Game.Screens.Menu
.FadeOut(beatLength, Easing.In);
}
private void updateColour()
protected virtual Color4 GetBaseColour()
{
Color4 baseColour = colours.Blue;
if (SeasonalUI.ENABLED)
baseColour = RNG.NextBool() ? SeasonalUI.PRIMARY_COLOUR_1 : SeasonalUI.PRIMARY_COLOUR_2;
else if (user.Value?.IsSupporter ?? false)
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();
+2 -1
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)];
+22 -75
View File
@@ -4,7 +4,6 @@
#nullable disable
using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -54,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;
@@ -152,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[]
@@ -212,15 +215,6 @@ namespace osu.Game.Screens.Menu
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
SeasonalUI.ENABLED
? hat = new Sprite
{
BypassAutoSizeAxes = Axes.Both,
Alpha = 0,
Origin = Anchor.BottomCentre,
Scale = new Vector2(-1, 1),
}
: Empty(),
}
},
impactContainer = new CircularContainer
@@ -253,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>
@@ -282,20 +278,11 @@ namespace osu.Game.Screens.Menu
{
sampleClick = audio.Samples.Get(@"Menu/osu-logo-select");
if (SeasonalUI.ENABLED)
{
sampleDownbeat = sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat-bell");
}
else
{
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");
if (hat != null)
hat.Texture = textures.Get(@"Menu/hat");
}
private int lastBeatIndex;
@@ -318,15 +305,13 @@ namespace osu.Game.Screens.Menu
{
if (beatIndex % timingPoint.TimeSignature.Numerator == 0)
{
sampleDownbeat?.Play();
SampleDownbeat?.Play();
}
else
{
var channel = sampleBeat.GetChannel();
if (SeasonalUI.ENABLED)
channel.Frequency.Value = 0.99 + RNG.NextDouble(0.02);
else
channel.Frequency.Value = 0.95 + RNG.NextDouble(0.1);
var channel = SampleBeat.GetChannel();
channel.Frequency.Value = 1 - BeatSampleVariance / 2 + RNG.NextDouble(BeatSampleVariance);
channel.Play();
}
});
@@ -381,9 +366,6 @@ namespace osu.Game.Screens.Menu
const float scale_adjust_cutoff = 0.4f;
if (SeasonalUI.ENABLED)
updateHat();
if (musicController.CurrentTrack.IsRunning)
{
float maxAmplitude = lastBeatIndex >= 0 ? musicController.CurrentTrack.CurrentAmplitudes.Maximum : 0;
@@ -397,38 +379,6 @@ namespace osu.Game.Screens.Menu
}
}
private bool hasHat;
private void updateHat()
{
if (hat == null)
return;
bool shouldHat = DrawWidth * Scale.X < 400;
if (shouldHat != hasHat)
{
hasHat = shouldHat;
if (hasHat)
{
hat.Delay(400)
.Then()
.MoveTo(new Vector2(120, 160))
.RotateTo(0)
.RotateTo(-20, 500, Easing.OutQuint)
.FadeIn(250, Easing.OutQuint);
}
else
{
hat.Delay(100)
.Then()
.MoveToOffset(new Vector2(0, -5), 500, Easing.OutQuint)
.FadeOut(500, Easing.OutQuint);
}
}
}
public override bool HandlePositionalInput => base.HandlePositionalInput && Alpha > 0.2f;
protected override bool OnMouseDown(MouseDownEvent e)
@@ -506,9 +456,6 @@ namespace osu.Game.Screens.Menu
private Container currentProxyTarget;
private Drawable proxy;
[CanBeNull]
private readonly Sprite hat;
public void StopSamplePlayback() => sampleClickChannel?.Stop();
public Drawable ProxyToContainer(Container c)
@@ -19,6 +19,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved(CanBeNull = true)]
private ILocalUserPlayInfo? localUserInfo { get; set; }
protected new ChatTextBox TextBox => base.TextBox!;
private readonly IBindable<LocalUserPlayingState> localUserPlaying = new Bindable<LocalUserPlayingState>();
public override bool PropagatePositionalInputSubTree => localUserPlaying.Value != LocalUserPlayingState.Playing;
@@ -58,7 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
localUserPlaying.BindValueChanged(playing =>
{
// for now let's never hold focus. this avoid misdirected gameplay keys entering chat.
// for now let's never hold focus. this avoids misdirected gameplay keys entering chat.
// note that this is done within this callback as it triggers an un-focus as well.
TextBox.HoldFocus = false;
@@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Overlays.Volume;
namespace osu.Game.Screens.Play
{
/// <summary>
/// Primarily handles volume adjustment in gameplay.
///
/// - If the user has mouse wheel disabled, only allow during break time or when holding alt. Also block scroll from parent handling.
/// - Otherwise always allow, as per <see cref="GlobalScrollAdjustsVolume"/> implementation.
/// </summary>
internal partial class GameplayScrollWheelHandling : GlobalScrollAdjustsVolume
{
private Bindable<bool> mouseWheelDisabled = null!;
[Resolved]
private IGameplayClock gameplayClock { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel);
}
protected override bool OnScroll(ScrollEvent e)
{
// During pause, allow global volume adjust regardless of settings.
if (gameplayClock.IsPaused.Value)
return base.OnScroll(e);
// Block any parent handling of scroll if the user has asked for it (special case when holding "Alt").
if (mouseWheelDisabled.Value && !e.AltPressed)
return true;
return base.OnScroll(e);
}
}
}
+59 -20
View File
@@ -8,7 +8,9 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Localisation.SkinComponents;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osuTK;
@@ -20,9 +22,24 @@ namespace osu.Game.Screens.Play.HUD
/// </summary>
public partial class ModDisplay : CompositeDrawable, IHasCurrentValue<IReadOnlyList<Mod>>
{
private const int fade_duration = 1000;
public const float MOD_ICON_SCALE = 0.6f;
public ExpansionMode ExpansionMode = ExpansionMode.ExpandOnHover;
private ExpansionMode expansionMode = ExpansionMode.ExpandOnHover;
public ExpansionMode ExpansionMode
{
get => expansionMode;
set
{
if (expansionMode == value)
return;
expansionMode = value;
if (IsLoaded)
updateExpansionMode();
}
}
private readonly BindableWithCurrent<IReadOnlyList<Mod>> current = new BindableWithCurrent<IReadOnlyList<Mod>>(Array.Empty<Mod>());
@@ -37,7 +54,19 @@ namespace osu.Game.Screens.Play.HUD
}
}
private readonly bool showExtendedInformation;
private bool showExtendedInformation;
public bool ShowExtendedInformation
{
get => showExtendedInformation;
set
{
showExtendedInformation = value;
foreach (var icon in iconsContainer)
icon.ShowExtendedInformation = value;
}
}
private readonly FillFlowContainer<ModIcon> iconsContainer;
public ModDisplay(bool showExtendedInformation = true)
@@ -58,11 +87,7 @@ namespace osu.Game.Screens.Play.HUD
base.LoadComplete();
Current.BindValueChanged(updateDisplay, true);
iconsContainer.FadeInFromZero(fade_duration, Easing.OutQuint);
if (ExpansionMode == ExpansionMode.AlwaysExpanded || ExpansionMode == ExpansionMode.AlwaysContracted)
FinishTransforms(true);
updateExpansionMode(0);
}
private void updateDisplay(ValueChangedEvent<IReadOnlyList<Mod>> mods)
@@ -70,29 +95,40 @@ namespace osu.Game.Screens.Play.HUD
iconsContainer.Clear();
foreach (Mod mod in mods.NewValue.AsOrdered())
iconsContainer.Add(new ModIcon(mod, showExtendedInformation: showExtendedInformation) { Scale = new Vector2(0.6f) });
appearTransform();
iconsContainer.Add(new ModIcon(mod, showExtendedInformation: showExtendedInformation) { Scale = new Vector2(MOD_ICON_SCALE) });
}
private void appearTransform()
private void updateExpansionMode(double duration = 500)
{
expand();
switch (expansionMode)
{
case ExpansionMode.AlwaysExpanded:
expand(duration);
break;
using (iconsContainer.BeginDelayedSequence(1200))
contract();
case ExpansionMode.AlwaysContracted:
contract(duration);
break;
case ExpansionMode.ExpandOnHover:
if (IsHovered)
expand(duration);
else
contract(duration);
break;
}
}
private void expand()
private void expand(double duration = 500)
{
if (ExpansionMode != ExpansionMode.AlwaysContracted)
iconsContainer.TransformSpacingTo(new Vector2(5, 0), 500, Easing.OutQuint);
iconsContainer.TransformSpacingTo(new Vector2(5, 0), duration, Easing.OutQuint);
}
private void contract()
private void contract(double duration = 500)
{
if (ExpansionMode != ExpansionMode.AlwaysExpanded)
iconsContainer.TransformSpacingTo(new Vector2(-25, 0), 500, Easing.OutQuint);
iconsContainer.TransformSpacingTo(new Vector2(-25, 0), duration, Easing.OutQuint);
}
protected override bool OnHover(HoverEvent e)
@@ -113,16 +149,19 @@ namespace osu.Game.Screens.Play.HUD
/// <summary>
/// The <see cref="ModDisplay"/> will expand only when hovered.
/// </summary>
[LocalisableDescription(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.ExpandOnHover))]
ExpandOnHover,
/// <summary>
/// The <see cref="ModDisplay"/> will always be expanded.
/// </summary>
[LocalisableDescription(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.AlwaysExpanded))]
AlwaysExpanded,
/// <summary>
/// The <see cref="ModDisplay"/> will always be contracted.
/// </summary>
AlwaysContracted
[LocalisableDescription(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.AlwaysContracted))]
AlwaysContracted,
}
}
@@ -0,0 +1,59 @@
// 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.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Localisation.SkinComponents;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// Displays a single-line horizontal auto-sized flow of mods. For cases where wrapping is required, use <see cref="ModFlowDisplay"/> instead.
/// </summary>
public partial class SkinnableModDisplay : CompositeDrawable, ISerialisableDrawable
{
private ModDisplay modDisplay = null!;
[Resolved]
private Bindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
[SettingSource(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.ShowExtendedInformation), nameof(SkinnableModDisplayStrings.ShowExtendedInformationDescription))]
public Bindable<bool> ShowExtendedInformation { get; } = new Bindable<bool>(true);
[SettingSource(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.ExpansionMode), nameof(SkinnableModDisplayStrings.ExpansionModeDescription))]
public Bindable<ExpansionMode> ExpansionModeSetting { get; } = new Bindable<ExpansionMode>();
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
// Provide a minimum autosize.
new Container { Size = ModIcon.MOD_ICON_SIZE * ModDisplay.MOD_ICON_SCALE },
modDisplay = new ModDisplay(),
};
modDisplay.Current = mods;
AutoSizeAxes = Axes.Both;
}
protected override void LoadComplete()
{
base.LoadComplete();
ShowExtendedInformation.BindValueChanged(_ => modDisplay.ShowExtendedInformation = ShowExtendedInformation.Value, true);
ExpansionModeSetting.BindValueChanged(_ => modDisplay.ExpansionMode = ExpansionModeSetting.Value, true);
FinishTransforms(true);
}
public bool UsesFixedAnchor { get; set; }
}
}
+4 -2
View File
@@ -85,7 +85,6 @@ namespace osu.Game.Screens.Play
private readonly BindableBool replayLoaded = new BindableBool();
private static bool hasShownNotificationOnce;
private readonly FillFlowContainer bottomRightElements;
internal readonly FillFlowContainer TopRightElements;
@@ -238,7 +237,7 @@ namespace osu.Game.Screens.Play
{
if (e.NewValue)
{
ModDisplay.FadeIn(200);
ModDisplay.FadeIn(1000, FADE_EASING);
InputCountController.Margin = new MarginPadding(10) { Bottom = 30 };
}
else
@@ -249,6 +248,9 @@ namespace osu.Game.Screens.Play
updateVisibility();
}, true);
ModDisplay.ExpansionMode = ExpansionMode.AlwaysExpanded;
Scheduler.AddDelayed(() => ModDisplay.ExpansionMode = ExpansionMode.ExpandOnHover, 1200);
}
protected override void Update()
+6 -17
View File
@@ -15,7 +15,6 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Framework.Threading;
@@ -88,8 +87,6 @@ namespace osu.Game.Screens.Play
private bool isRestarting;
private bool skipExitTransition;
private Bindable<bool> mouseWheelDisabled;
private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>();
public IBindable<bool> LocalUserPlaying => localUserPlaying;
@@ -228,8 +225,6 @@ namespace osu.Game.Screens.Play
return;
}
mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel);
if (game != null)
gameActive.BindTo(game.IsActive);
@@ -251,7 +246,10 @@ namespace osu.Game.Screens.Play
dependencies.CacheAs(HealthProcessor);
InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime);
InternalChildren = new Drawable[]
{
GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime),
};
AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer));
@@ -266,6 +264,7 @@ namespace osu.Game.Screens.Play
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, HealthProcessor, Beatmap.Value.Storyboard));
var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin);
GameplayClockContainer.Add(new GameplayScrollWheelHandling());
// load the skinning hierarchy first.
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
@@ -894,16 +893,6 @@ namespace osu.Game.Screens.Play
});
}
protected override bool OnScroll(ScrollEvent e)
{
// During pause, allow global volume adjust regardless of settings.
if (GameplayClockContainer.IsPaused.Value)
return false;
// Block global volume adjust if the user has asked for it (special case when holding "Alt").
return mouseWheelDisabled.Value && !e.AltPressed;
}
#region Gameplay leaderboard
protected readonly Bindable<bool> LeaderboardExpandedState = new BindableBool();
@@ -1032,7 +1021,7 @@ namespace osu.Game.Screens.Play
private double? lastPauseActionTime;
protected bool PauseCooldownActive =>
lastPauseActionTime.HasValue && GameplayClockContainer.CurrentTime < lastPauseActionTime + PauseCooldownDuration;
PlayingState.Value == LocalUserPlayingState.Playing && lastPauseActionTime.HasValue && GameplayClockContainer.CurrentTime < lastPauseActionTime + PauseCooldownDuration;
/// <summary>
/// A set of conditionals which defines whether the current game state and configuration allows for
+2
View File
@@ -27,6 +27,7 @@ using osu.Game.Input;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.Volume;
using osu.Game.Performance;
using osu.Game.Scoring;
using osu.Game.Screens.Menu;
@@ -190,6 +191,7 @@ namespace osu.Game.Screens.Play
InternalChildren = new Drawable[]
{
new GlobalScrollAdjustsVolume(),
(content = new LogoTrackingContainer
{
Anchor = Anchor.Centre,
@@ -88,6 +88,9 @@ namespace osu.Game.Screens.Select.Carousel
[Resolved]
private OsuGame? game { get; set; }
[Resolved]
private BeatmapManager? manager { get; set; }
private IBindable<StarDifficulty?> starDifficultyBindable = null!;
private CancellationTokenSource? starDifficultyCancellationSource;
@@ -98,7 +101,7 @@ namespace osu.Game.Screens.Select.Carousel
}
[BackgroundDependencyLoader]
private void load(BeatmapManager? manager, SongSelect? songSelect)
private void load(SongSelect? songSelect)
{
Header.Height = height;
@@ -300,6 +303,9 @@ namespace osu.Game.Screens.Select.Carousel
if (beatmapInfo.GetOnlineURL(api, ruleset.Value) is string url)
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyUrlToClipboard(url)));
if (manager != null)
items.Add(new OsuMenuItem("Mark as played", MenuItemType.Standard, () => manager.MarkPlayed(beatmapInfo)));
if (hideRequested != null)
items.Add(new OsuMenuItem(WebCommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => hideRequested(beatmapInfo)));
+4 -1
View File
@@ -31,6 +31,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Volume;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Backgrounds;
@@ -169,10 +170,12 @@ namespace osu.Game.Screens.Select
AddRangeInternal(new Drawable[]
{
new GlobalScrollAdjustsVolume(),
new VerticalMaskingContainer
{
Children = new Drawable[]
{
new GlobalScrollAdjustsVolume(),
new GridContainer // used for max width implementation
{
RelativeSizeAxes = Axes.Both,
@@ -375,7 +378,7 @@ namespace osu.Game.Screens.Select
BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show());
BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => DeleteBeatmap(Beatmap.Value.BeatmapSetInfo));
BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null);
BeatmapOptions.AddButton(@"Mark", @"as played", FontAwesome.Regular.TimesCircle, colours.Purple, () => beatmaps.MarkPlayed(Beatmap.Value.BeatmapInfo));
BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => ClearScores(Beatmap.Value.BeatmapInfo));
}
@@ -15,14 +15,18 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Menu;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
namespace osu.Game.Seasonal
{
public partial class IntroChristmas : IntroScreen
{
protected override string BeatmapHash => "7e26183e72a496f672c3a21292e6b469fdecd084d31c259ea10a31df5b46cd77";
// nekodex - circle the halls
public const string CHRISTMAS_BEATMAP_SET_HASH = "7e26183e72a496f672c3a21292e6b469fdecd084d31c259ea10a31df5b46cd77";
protected override string BeatmapHash => CHRISTMAS_BEATMAP_SET_HASH;
protected override string BeatmapFile => "christmas2024.osz";
@@ -299,7 +303,7 @@ namespace osu.Game.Screens.Menu
float x = 0.5f + 0.5f * randomRadius * (float)Math.Cos(angle);
float y = 0.5f + 0.5f * randomRadius * (float)Math.Sin(angle);
Color4 christmasColour = RNG.NextBool() ? SeasonalUI.PRIMARY_COLOUR_1 : SeasonalUI.PRIMARY_COLOUR_2;
Color4 christmasColour = RNG.NextBool() ? SeasonalUIConfig.PRIMARY_COLOUR_1 : SeasonalUIConfig.PRIMARY_COLOUR_2;
Drawable triangle = new Triangle
{
@@ -21,21 +21,23 @@ using osu.Game.Rulesets.Objects.Types;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
namespace osu.Game.Seasonal
{
public partial class MainMenuSeasonalLighting : CompositeDrawable
{
private IBindable<WorkingBeatmap> working = null!;
private InterpolatingFramedClock beatmapClock = null!;
private InterpolatingFramedClock? beatmapClock;
private List<HitObject> hitObjects = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
private RulesetInfo? osuRuleset;
private int? lastObjectIndex;
public MainMenuSeasonalLighting()
{
// match beatmap playfield
RelativeChildSize = new Vector2(512, 384);
RelativeSizeAxes = Axes.X;
@@ -45,8 +47,11 @@ namespace osu.Game.Screens.Menu
}
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> working)
private void load(IBindable<WorkingBeatmap> working, RulesetStore rulesets)
{
// operate in osu! ruleset to keep things simple for now.
osuRuleset = rulesets.GetRuleset(0);
this.working = working.GetBoundCopy();
this.working.BindValueChanged(_ => Scheduler.AddOnce(updateBeatmap), true);
}
@@ -54,18 +59,32 @@ namespace osu.Game.Screens.Menu
private void updateBeatmap()
{
lastObjectIndex = null;
if (osuRuleset == null)
{
beatmapClock = new InterpolatingFramedClock(Clock);
hitObjects = new List<HitObject>();
return;
}
// Intentionally maintain separately so the lighting is not in audio clock space (it shouldn't rewind etc.)
beatmapClock = new InterpolatingFramedClock(new FramedClock(working.Value.Track));
hitObjects = working.Value.GetPlayableBeatmap(rulesets.GetRuleset(0)).HitObjects.SelectMany(h => h.NestedHitObjects.Prepend(h))
hitObjects = working.Value
.GetPlayableBeatmap(osuRuleset)
.HitObjects
.SelectMany(h => h.NestedHitObjects.Prepend(h))
.OrderBy(h => h.StartTime)
.ToList();
}
private int? lastObjectIndex;
protected override void Update()
{
base.Update();
if (osuRuleset == null || beatmapClock == null)
return;
Height = DrawWidth / 16 * 10;
beatmapClock.ProcessFrame();
@@ -105,7 +124,7 @@ namespace osu.Game.Screens.Menu
if (h.GetType().Name.Contains("Tick"))
{
light.Colour = SeasonalUI.AMBIENT_COLOUR_1;
light.Colour = SeasonalUIConfig.AMBIENT_COLOUR_1;
light.Scale = new Vector2(0.5f);
light
.FadeInFromZero(250)
@@ -116,19 +135,19 @@ namespace osu.Game.Screens.Menu
}
else
{
// default green
Color4 col = SeasonalUI.PRIMARY_COLOUR_2;
// default are green
Color4 col = SeasonalUIConfig.PRIMARY_COLOUR_2;
// whistle red
// whistles are red
if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE))
col = SeasonalUI.PRIMARY_COLOUR_1;
// clap is third colour
col = SeasonalUIConfig.PRIMARY_COLOUR_1;
// clap is third ambient (yellow) colour
else if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP))
col = SeasonalUI.AMBIENT_COLOUR_1;
col = SeasonalUIConfig.AMBIENT_COLOUR_1;
light.Colour = col;
// finish larger lighting
// finish results in larger lighting
if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH))
light.Scale = new Vector2(3);
@@ -141,7 +160,7 @@ namespace osu.Game.Screens.Menu
light.Expire();
}
public partial class Light : CompositeDrawable
private partial class Light : CompositeDrawable
{
private readonly Circle circle;
@@ -168,12 +187,12 @@ namespace osu.Game.Screens.Menu
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(12),
Colour = SeasonalUI.AMBIENT_COLOUR_1,
Colour = SeasonalUIConfig.AMBIENT_COLOUR_1,
Blending = BlendingParameters.Additive,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = SeasonalUI.AMBIENT_COLOUR_2,
Colour = SeasonalUIConfig.AMBIENT_COLOUR_2,
Radius = 80,
}
}
+76
View File
@@ -0,0 +1,76 @@
// 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.Audio;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Screens.Menu;
using osuTK;
namespace osu.Game.Seasonal
{
public partial class OsuLogoChristmas : OsuLogo
{
protected override double BeatSampleVariance => 0.02;
private Sprite? hat;
private bool hasHat;
protected override MenuLogoVisualisation CreateMenuLogoVisualisation() => new SeasonalMenuLogoVisualisation();
[BackgroundDependencyLoader]
private void load(TextureStore textures, AudioManager audio)
{
LogoElements.Add(hat = new Sprite
{
BypassAutoSizeAxes = Axes.Both,
Alpha = 0,
Origin = Anchor.BottomCentre,
Scale = new Vector2(-1, 1),
Texture = textures.Get(@"Menu/hat"),
});
// override base samples with our preferred ones.
SampleDownbeat = SampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat-bell");
}
protected override void Update()
{
base.Update();
updateHat();
}
private void updateHat()
{
if (hat == null)
return;
bool shouldHat = DrawWidth * Scale.X < 400;
if (shouldHat != hasHat)
{
hasHat = shouldHat;
if (hasHat)
{
hat.Delay(400)
.Then()
.MoveTo(new Vector2(120, 160))
.RotateTo(0)
.RotateTo(-20, 500, Easing.OutQuint)
.FadeIn(250, Easing.OutQuint);
}
else
{
hat.Delay(100)
.Then()
.MoveToOffset(new Vector2(0, -5), 500, Easing.OutQuint)
.FadeOut(500, Easing.OutQuint);
}
}
}
}
}
@@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Screens.Menu;
namespace osu.Game.Seasonal
{
internal partial class SeasonalMenuLogoVisualisation : MenuLogoVisualisation
{
protected override void UpdateColour() => Colour = SeasonalUIConfig.AMBIENT_COLOUR_1;
}
}
@@ -0,0 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Utils;
using osu.Game.Screens.Menu;
using osuTK.Graphics;
namespace osu.Game.Seasonal
{
public partial class SeasonalMenuSideFlashes : MenuSideFlashes
{
protected override bool RefreshColoursEveryFlash => true;
protected override float Intensity => 4;
protected override Color4 GetBaseColour() => RNG.NextBool() ? SeasonalUIConfig.PRIMARY_COLOUR_1 : SeasonalUIConfig.PRIMARY_COLOUR_2;
}
}
@@ -4,18 +4,21 @@
using osu.Framework.Extensions.Color4Extensions;
using osuTK.Graphics;
namespace osu.Game.Screens
namespace osu.Game.Seasonal
{
public static class SeasonalUI
/// <summary>
/// General configuration setting for seasonal event adjustments to the game.
/// </summary>
public static class SeasonalUIConfig
{
public static readonly bool ENABLED = true;
public static readonly Color4 PRIMARY_COLOUR_1 = Color4Extensions.FromHex("D32F2F");
public static readonly Color4 PRIMARY_COLOUR_1 = Color4Extensions.FromHex(@"D32F2F");
public static readonly Color4 PRIMARY_COLOUR_2 = Color4Extensions.FromHex("388E3C");
public static readonly Color4 PRIMARY_COLOUR_2 = Color4Extensions.FromHex(@"388E3C");
public static readonly Color4 AMBIENT_COLOUR_1 = Color4Extensions.FromHex("FFC");
public static readonly Color4 AMBIENT_COLOUR_1 = Color4Extensions.FromHex(@"FFFFCC");
public static readonly Color4 AMBIENT_COLOUR_2 = Color4Extensions.FromHex("FFE4B5");
public static readonly Color4 AMBIENT_COLOUR_2 = Color4Extensions.FromHex(@"FFE4B5");
}
}
+2 -2
View File
@@ -35,8 +35,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.1206.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.1219.1" />
<PackageReference Include="ppy.osu.Framework" Version="2024.1224.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.1224.0" />
<PackageReference Include="Sentry" Version="4.13.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.38.0" />
+1 -1
View File
@@ -17,6 +17,6 @@
<MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.1206.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.1224.0" />
</ItemGroup>
</Project>
+14
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.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameApplicationDelegate
{
protected override Framework.Game CreateGame() => new OsuGameIOS();
}
}
+2
View File
@@ -17,6 +17,8 @@ namespace osu.iOS
{
public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString());
public override bool HideUnlicensedContent => true;
protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier();
protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo();
@@ -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.iOS
{
public static class Application
public static class Program
{
public static void Main(string[] args)
{
GameApplication.Main(new OsuGameIOS());
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}