Merge branch 'master' into editor-background

This commit is contained in:
Dean Herbert
2022-11-02 17:43:38 +09:00
124 changed files with 2953 additions and 1031 deletions

View File

@ -47,6 +47,7 @@ namespace osu.Game.Beatmaps
// Shallow clone isn't enough to ensure we don't mutate beatmap info unexpectedly.
// Can potentially be removed after `Beatmap.Difficulty` doesn't save back to `Beatmap.BeatmapInfo`.
original.BeatmapInfo = original.BeatmapInfo.Clone();
original.ControlPointInfo = original.ControlPointInfo.DeepClone();
return ConvertBeatmap(original, cancellationToken);
}

View File

@ -238,14 +238,6 @@ namespace osu.Game.Beatmaps
#region Compatibility properties
[Ignored]
[Obsolete("Use BeatmapInfo.Difficulty instead.")] // can be removed 20220719
public BeatmapDifficulty BaseDifficulty
{
get => Difficulty;
set => Difficulty = value;
}
[Ignored]
public string? Path => File?.Filename;

View File

@ -340,7 +340,7 @@ namespace osu.Game.Beatmaps
static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo)
{
var metadata = beatmapInfo.Metadata;
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename();
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidFilename();
}
}

View File

@ -3,7 +3,6 @@
#nullable disable
using System;
using System.Collections.Generic;
using osuTK.Graphics;
@ -22,11 +21,5 @@ namespace osu.Game.Beatmaps.Formats
/// if empty, <see cref="ComboColours"/> will fall back to default combo colours.
/// </summary>
List<Color4> CustomComboColours { get; }
/// <summary>
/// Adds combo colours to the list.
/// </summary>
[Obsolete("Use CustomComboColours directly.")] // can be removed 20220215
void AddComboColours(params Color4[] colours);
}
}

View File

@ -435,8 +435,10 @@ namespace osu.Game.Beatmaps.Formats
addControlPoint(time, controlPoint, true);
}
int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
#pragma warning disable 618
addControlPoint(time, new LegacyDifficultyControlPoint(beatLength)
addControlPoint(time, new LegacyDifficultyControlPoint(onlineRulesetID, beatLength)
#pragma warning restore 618
{
SliderVelocity = speedMultiplier,
@ -448,8 +450,6 @@ namespace osu.Game.Beatmaps.Formats
OmitFirstBarLine = omitFirstBarSignature,
};
int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
// osu!taiko and osu!mania use effect points rather than difficulty points for scroll speed adjustments.
if (onlineRulesetID == 1 || onlineRulesetID == 3)
effectPoint.ScrollSpeed = speedMultiplier;

View File

@ -174,11 +174,15 @@ namespace osu.Game.Beatmaps.Formats
/// </summary>
public bool GenerateTicks { get; private set; } = true;
public LegacyDifficultyControlPoint(double beatLength)
public LegacyDifficultyControlPoint(int rulesetId, double beatLength)
: this()
{
// Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?).
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1;
if (rulesetId == 1 || rulesetId == 3)
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1;
else
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 1000) / 100.0 : 1;
GenerateTicks = !double.IsNaN(beatLength);
}

View File

@ -1,20 +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 System.ComponentModel;
namespace osu.Game.Beatmaps.Timing
{
[Obsolete("Use osu.Game.Beatmaps.Timing.TimeSignature instead.")]
public enum TimeSignatures // can be removed 20220722
{
[Description("4/4")]
SimpleQuadruple = 4,
[Description("3/4")]
SimpleTriple = 3
}
}

View File

@ -1,51 +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.ComponentModel.DataAnnotations.Schema;
using osu.Game.Database;
namespace osu.Game.Configuration
{
[Table("Settings")]
public class DatabasedSetting : IHasPrimaryKey // can be removed 20220315.
{
public int ID { get; set; }
public bool IsManaged => ID > 0;
public int? RulesetID { get; set; }
public int? Variant { get; set; }
public int? SkinInfoID { get; set; }
[Column("Key")]
public string Key { get; set; }
[Column("Value")]
public string StringValue
{
get => Value.ToString();
set => Value = value;
}
public object Value;
public DatabasedSetting(string key, object value)
{
Key = key;
Value = value;
}
/// <summary>
/// Constructor for derived classes that may require serialisation.
/// </summary>
public DatabasedSetting()
{
}
public override string ToString() => $"{Key}=>{Value}";
}
}

View File

@ -118,7 +118,6 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.Prefer24HourTime, CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.Contains(@"tt"));
// Gameplay
SetDefault(OsuSetting.PositionalHitsounds, true); // replaced by level setting below, can be removed 20220703.
SetDefault(OsuSetting.PositionalHitsoundsLevel, 0.2f, 0, 1);
SetDefault(OsuSetting.DimLevel, 0.7, 0, 1, 0.01);
SetDefault(OsuSetting.BlurLevel, 0, 0, 1, 0.01);
@ -128,7 +127,6 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.HitLighting, true);
SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always);
SetDefault(OsuSetting.ShowProgressGraph, true);
SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true);
SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true);
SetDefault(OsuSetting.KeyOverlay, false);
@ -155,6 +153,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.SongSelectRightMouseScroll, false);
SetDefault(OsuSetting.Scaling, ScalingMode.Off);
SetDefault(OsuSetting.SafeAreaConsiderations, true);
SetDefault(OsuSetting.ScalingSizeX, 0.8f, 0.2f, 1f);
SetDefault(OsuSetting.ScalingSizeY, 0.8f, 0.2f, 1f);
@ -204,14 +203,11 @@ namespace osu.Game.Configuration
if (!int.TryParse(pieces[0], out int year)) return;
if (!int.TryParse(pieces[1], out int monthDay)) return;
// ReSharper disable once UnusedVariable
int combined = (year * 10000) + monthDay;
if (combined < 20220103)
{
var positionalHitsoundsEnabled = GetBindable<bool>(OsuSetting.PositionalHitsounds);
if (!positionalHitsoundsEnabled.Value)
SetValue(OsuSetting.PositionalHitsoundsLevel, 0);
}
// migrations can be added here using a condition like:
// if (combined < 20220103) { performMigration() }
}
public override TrackedSettings CreateTrackedSettings()
@ -298,14 +294,11 @@ namespace osu.Game.Configuration
ShowStoryboard,
KeyOverlay,
GameplayLeaderboard,
PositionalHitsounds,
PositionalHitsoundsLevel,
AlwaysPlayFirstComboBreak,
FloatingComments,
HUDVisibilityMode,
// This has been migrated to the component itself. can be removed 20221027.
ShowProgressGraph,
ShowHealthDisplayWhenCantFail,
FadePlayfieldWhenHealthLow,
MouseDisableButtons,
@ -372,6 +365,7 @@ namespace osu.Game.Configuration
DiscordRichPresence,
AutomaticallyDownloadWhenSpectating,
ShowOnlineExplicitContent,
LastProcessedMetadataId
LastProcessedMetadataId,
SafeAreaConsiderations,
}
}

View File

@ -37,7 +37,7 @@ namespace osu.Game.Database
/// <param name="item">The item to export.</param>
public void Export(TModel item)
{
string filename = $"{item.GetDisplayString().GetValidArchiveContentFilename()}{FileExtension}";
string filename = $"{item.GetDisplayString().GetValidFilename()}{FileExtension}";
using (var stream = exportStorage.CreateFileSafely(filename))
ExportModelTo(item, stream);

View File

@ -857,17 +857,7 @@ namespace osu.Game.Database
if (legacyCollectionImporter.GetAvailableCount(storage).GetResultSafely() > 0)
{
legacyCollectionImporter.ImportFromStorage(storage).ContinueWith(task =>
{
if (task.Exception != null)
{
// can be removed 20221027 (just for initial safety).
Logger.Error(task.Exception.InnerException, "Collections could not be migrated to realm. Please provide your \"collection.db\" to the dev team.");
return;
}
storage.Move("collection.db", "collection.db.migrated");
});
legacyCollectionImporter.ImportFromStorage(storage).ContinueWith(_ => storage.Move("collection.db", "collection.db.migrated"));
}
break;

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO;
@ -15,6 +15,8 @@ namespace osu.Game.Extensions
{
public static class ModelExtensions
{
private static readonly Regex invalid_filename_chars = new Regex(@"(?!$)[^A-Za-z0-9_()[\]. \-]", RegexOptions.Compiled);
/// <summary>
/// Get the relative path in osu! storage for this file.
/// </summary>
@ -137,20 +139,14 @@ namespace osu.Game.Extensions
return instance.OnlineID.Equals(other.OnlineID);
}
private static readonly char[] invalid_filename_characters = Path.GetInvalidFileNameChars()
// Backslash is added to avoid issues when exporting to zip.
// See SharpCompress filename normalisation https://github.com/adamhathcock/sharpcompress/blob/a1e7c0068db814c9aa78d86a94ccd1c761af74bd/src/SharpCompress/Writers/Zip/ZipWriter.cs#L143.
.Append('\\')
.ToArray();
/// <summary>
/// Get a valid filename for use inside a zip file. Avoids backslashes being incorrectly converted to directories.
/// Create a valid filename which should work across all platforms.
/// </summary>
public static string GetValidArchiveContentFilename(this string filename)
{
foreach (char c in invalid_filename_characters)
filename = filename.Replace(c, '_');
return filename;
}
/// <remarks>
/// This function replaces all characters not included in a very pessimistic list which should be compatible
/// across all operating systems. We are using this in place of <see cref="Path.GetInvalidFileNameChars"/> as
/// that function does not have per-platform considerations (and is only made to work on windows).
/// </remarks>
public static string GetValidFilename(this string filename) => invalid_filename_chars.Replace(filename, "_");
}
}

View File

@ -29,6 +29,7 @@ namespace osu.Game.Graphics.Containers
private Bindable<float> sizeY;
private Bindable<float> posX;
private Bindable<float> posY;
private Bindable<bool> applySafeAreaPadding;
private Bindable<MarginPadding> safeAreaPadding;
@ -132,6 +133,9 @@ namespace osu.Game.Graphics.Containers
posY = config.GetBindable<float>(OsuSetting.ScalingPositionY);
posY.ValueChanged += _ => Scheduler.AddOnce(updateSize);
applySafeAreaPadding = config.GetBindable<bool>(OsuSetting.SafeAreaConsiderations);
applySafeAreaPadding.BindValueChanged(_ => Scheduler.AddOnce(updateSize));
safeAreaPadding = safeArea.SafeAreaPadding.GetBoundCopy();
safeAreaPadding.BindValueChanged(_ => Scheduler.AddOnce(updateSize));
}
@ -192,7 +196,7 @@ namespace osu.Game.Graphics.Containers
bool requiresMasking = targetRect.Size != Vector2.One
// For the top level scaling container, for now we apply masking if safe areas are in use.
// In the future this can likely be removed as more of the actual UI supports overflowing into the safe areas.
|| (targetMode == ScalingMode.Everything && safeAreaPadding.Value.Total != Vector2.Zero);
|| (targetMode == ScalingMode.Everything && (applySafeAreaPadding.Value && safeAreaPadding.Value.Total != Vector2.Zero));
if (requiresMasking)
sizableContainer.Masking = true;
@ -225,6 +229,9 @@ namespace osu.Game.Graphics.Containers
[Resolved]
private ISafeArea safeArea { get; set; }
[Resolved]
private OsuConfigManager config { get; set; }
private readonly bool confineHostCursor;
private readonly LayoutValue cursorRectCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
@ -259,7 +266,7 @@ namespace osu.Game.Graphics.Containers
{
if (host.Window == null) return;
bool coversWholeScreen = Size == Vector2.One && safeArea.SafeAreaPadding.Value.Total == Vector2.Zero;
bool coversWholeScreen = Size == Vector2.One && (!config.Get<bool>(OsuSetting.SafeAreaConsiderations) || safeArea.SafeAreaPadding.Value.Total == Vector2.Zero);
host.Window.CursorConfineRect = coversWholeScreen ? null : ToScreenSpace(DrawRectangle).AABBFloat;
}
}

View File

@ -25,10 +25,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuBreadcrumbDisplayDirectory(directory, displayName);
[BackgroundDependencyLoader]
private void load()
public OsuDirectorySelectorBreadcrumbDisplay()
{
Height = 50;
Padding = new MarginPadding(15);
}
private class OsuBreadcrumbDisplayComputer : OsuBreadcrumbDisplayDirectory

View File

@ -31,14 +31,17 @@ namespace osu.Game.Input.Bindings
parentInputManager = GetContainingInputManager();
}
// IMPORTANT: Do not change the order of key bindings in this list.
// It is used to decide the order of precedence (see note in DatabasedKeyBindingContainer).
// IMPORTANT: Take care when changing order of the items in the enumerable.
// It is used to decide the order of precedence, with the earlier items having higher precedence.
public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings
.Concat(OverlayKeyBindings)
.Concat(EditorKeyBindings)
.Concat(InGameKeyBindings)
.Concat(SongSelectKeyBindings)
.Concat(AudioControlKeyBindings);
.Concat(AudioControlKeyBindings)
// Overlay bindings may conflict with more local cases like the editor so they are checked last.
// It has generally been agreed on that local screens like the editor should have priority,
// based on such usages potentially requiring a lot more key bindings that may be "shared" with global ones.
.Concat(OverlayKeyBindings);
public IEnumerable<KeyBinding> GlobalKeyBindings => new[]
{
@ -87,6 +90,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.EditorTimingMode),
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.EditorSetupMode),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode),
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.EditorCloneSelection),
new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft),
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode),
@ -343,5 +347,8 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleProfile))]
ToggleProfile,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCloneSelection))]
EditorCloneSelection
}
}

View File

@ -184,6 +184,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString EditorTapForBPM => new TranslatableString(getKey(@"editor_tap_for_bpm"), @"Tap for BPM");
/// <summary>
/// "Clone selection"
/// </summary>
public static LocalisableString EditorCloneSelection => new TranslatableString(getKey(@"editor_clone_selection"), @"Clone selection");
/// <summary>
/// "Cycle grid display mode"
/// </summary>

View File

@ -44,7 +44,8 @@ namespace osu.Game.Online.API.Requests.Responses
public int MaxCombo { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty("rank")]
// ScoreRank is aligned to make 0 equal D. We still want to serialise this (even when DefaultValueHandling.Ignore is used).
[JsonProperty("rank", DefaultValueHandling = DefaultValueHandling.Include)]
public ScoreRank Rank { get; set; }
[JsonProperty("started_at")]
@ -153,10 +154,8 @@ namespace osu.Game.Online.API.Requests.Responses
var mods = Mods.Select(apiMod => apiMod.ToMod(rulesetInstance)).ToArray();
var scoreInfo = ToScoreInfo(mods);
var scoreInfo = ToScoreInfo(mods, beatmap);
scoreInfo.Ruleset = ruleset;
if (beatmap != null) scoreInfo.BeatmapInfo = beatmap;
return scoreInfo;
}
@ -165,25 +164,47 @@ namespace osu.Game.Online.API.Requests.Responses
/// Create a <see cref="ScoreInfo"/> from an API score instance.
/// </summary>
/// <param name="mods">The mod instances, resolved from a ruleset.</param>
/// <returns></returns>
public ScoreInfo ToScoreInfo(Mod[] mods) => new ScoreInfo
/// <param name="beatmap">The object to populate the scores' beatmap with.
///<list type="bullet">
/// <item>If this is a <see cref="BeatmapInfo"/> type, then the score will be fully populated with the given object.</item>
/// <item>Otherwise, if this is an <see cref="IBeatmapInfo"/> type (e.g. <see cref="APIBeatmap"/>), then only the beatmap ruleset will be populated.</item>
/// <item>Otherwise, if this is <c>null</c>, then the beatmap ruleset will not be populated.</item>
/// <item>The online beatmap ID is populated in all cases.</item>
/// </list>
/// </param>
/// <returns>The populated <see cref="ScoreInfo"/>.</returns>
public ScoreInfo ToScoreInfo(Mod[] mods, IBeatmapInfo? beatmap = null)
{
OnlineID = OnlineID,
User = User ?? new APIUser { Id = UserID },
BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID },
Ruleset = new RulesetInfo { OnlineID = RulesetID },
Passed = Passed,
TotalScore = TotalScore,
Accuracy = Accuracy,
MaxCombo = MaxCombo,
Rank = Rank,
Statistics = Statistics,
MaximumStatistics = MaximumStatistics,
Date = EndedAt,
Hash = HasReplay ? "online" : string.Empty, // TODO: temporary?
Mods = mods,
PP = PP,
};
var score = new ScoreInfo
{
OnlineID = OnlineID,
User = User ?? new APIUser { Id = UserID },
BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID },
Ruleset = new RulesetInfo { OnlineID = RulesetID },
Passed = Passed,
TotalScore = TotalScore,
Accuracy = Accuracy,
MaxCombo = MaxCombo,
Rank = Rank,
Statistics = Statistics,
MaximumStatistics = MaximumStatistics,
Date = EndedAt,
Hash = HasReplay ? "online" : string.Empty, // TODO: temporary?
Mods = mods,
PP = PP,
};
if (beatmap is BeatmapInfo realmBeatmap)
score.BeatmapInfo = realmBeatmap;
else if (beatmap != null)
{
score.BeatmapInfo.Ruleset.OnlineID = beatmap.Ruleset.OnlineID;
score.BeatmapInfo.Ruleset.Name = beatmap.Ruleset.Name;
score.BeatmapInfo.Ruleset.ShortName = beatmap.Ruleset.ShortName;
}
return score;
}
/// <summary>
/// Creates a <see cref="SoloScoreInfo"/> from a local score for score submission.

View File

@ -0,0 +1,28 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
namespace osu.Game.Online
{
public class HubClient : PersistentEndpointClient
{
public readonly HubConnection Connection;
public HubClient(HubConnection connection)
{
Connection = connection;
Connection.Closed += InvokeClosed;
}
public override Task ConnectAsync(CancellationToken cancellationToken) => Connection.StartAsync(cancellationToken);
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync().ConfigureAwait(false);
await Connection.DisposeAsync().ConfigureAwait(false);
}
}
}

View File

@ -10,13 +10,11 @@ using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using osu.Framework;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Online.API;
namespace osu.Game.Online
{
public class HubClientConnector : IHubClientConnector
public class HubClientConnector : PersistentEndpointClientConnector, IHubClientConnector
{
public const string SERVER_SHUTDOWN_MESSAGE = "Server is shutting down.";
@ -25,7 +23,6 @@ namespace osu.Game.Online
/// </summary>
public Action<HubConnection>? ConfigureConnection { get; set; }
private readonly string clientName;
private readonly string endpoint;
private readonly string versionHash;
private readonly bool preferMessagePack;
@ -34,18 +31,7 @@ namespace osu.Game.Online
/// <summary>
/// The current connection opened by this connector.
/// </summary>
public HubConnection? CurrentConnection { get; private set; }
/// <summary>
/// Whether this is connected to the hub, use <see cref="CurrentConnection"/> to access the connection, if this is <c>true</c>.
/// </summary>
public IBindable<bool> IsConnected => isConnected;
private readonly Bindable<bool> isConnected = new Bindable<bool>();
private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1);
private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
public new HubConnection? CurrentConnection => ((HubClient?)base.CurrentConnection)?.Connection;
/// <summary>
/// Constructs a new <see cref="HubClientConnector"/>.
@ -56,99 +42,16 @@ namespace osu.Game.Online
/// <param name="versionHash">The hash representing the current game version, used for verification purposes.</param>
/// <param name="preferMessagePack">Whether to use MessagePack for serialisation if available on this platform.</param>
public HubClientConnector(string clientName, string endpoint, IAPIProvider api, string versionHash, bool preferMessagePack = true)
: base(api)
{
this.clientName = clientName;
ClientName = clientName;
this.endpoint = endpoint;
this.api = api;
this.versionHash = versionHash;
this.preferMessagePack = preferMessagePack;
apiState.BindTo(api.State);
apiState.BindValueChanged(_ => Task.Run(connectIfPossible), true);
}
public Task Reconnect()
{
Logger.Log($"{clientName} reconnecting...", LoggingTarget.Network);
return Task.Run(connectIfPossible);
}
private async Task connectIfPossible()
{
switch (apiState.Value)
{
case APIState.Failing:
case APIState.Offline:
await disconnect(true);
break;
case APIState.Online:
await connect();
break;
}
}
private async Task connect()
{
cancelExistingConnect();
if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck.");
try
{
while (apiState.Value == APIState.Online)
{
// ensure any previous connection was disposed.
// this will also create a new cancellation token source.
await disconnect(false).ConfigureAwait(false);
// this token will be valid for the scope of this connection.
// if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere.
var cancellationToken = connectCancelSource.Token;
cancellationToken.ThrowIfCancellationRequested();
Logger.Log($"{clientName} connecting...", LoggingTarget.Network);
try
{
// importantly, rebuild the connection each attempt to get an updated access token.
CurrentConnection = buildConnection(cancellationToken);
await CurrentConnection.StartAsync(cancellationToken).ConfigureAwait(false);
Logger.Log($"{clientName} connected!", LoggingTarget.Network);
isConnected.Value = true;
return;
}
catch (OperationCanceledException)
{
//connection process was cancelled.
throw;
}
catch (Exception e)
{
await handleErrorAndDelay(e, cancellationToken).ConfigureAwait(false);
}
}
}
finally
{
connectionLock.Release();
}
}
/// <summary>
/// Handles an exception and delays an async flow.
/// </summary>
private async Task handleErrorAndDelay(Exception exception, CancellationToken cancellationToken)
{
Logger.Log($"{clientName} connect attempt failed: {exception.Message}", LoggingTarget.Network);
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
}
private HubConnection buildConnection(CancellationToken cancellationToken)
protected override Task<PersistentEndpointClient> BuildConnectionAsync(CancellationToken cancellationToken)
{
var builder = new HubConnectionBuilder()
.WithUrl(endpoint, options =>
@ -188,59 +91,9 @@ namespace osu.Game.Online
ConfigureConnection?.Invoke(newConnection);
newConnection.Closed += ex => onConnectionClosed(ex, cancellationToken);
return newConnection;
return Task.FromResult((PersistentEndpointClient)new HubClient(newConnection));
}
private async Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken)
{
isConnected.Value = false;
if (ex != null)
await handleErrorAndDelay(ex, cancellationToken).ConfigureAwait(false);
else
Logger.Log($"{clientName} disconnected", LoggingTarget.Network);
// make sure a disconnect wasn't triggered (and this is still the active connection).
if (!cancellationToken.IsCancellationRequested)
await Task.Run(connect, default).ConfigureAwait(false);
}
private async Task disconnect(bool takeLock)
{
cancelExistingConnect();
if (takeLock)
{
if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck.");
}
try
{
if (CurrentConnection != null)
await CurrentConnection.DisposeAsync().ConfigureAwait(false);
}
finally
{
CurrentConnection = null;
if (takeLock)
connectionLock.Release();
}
}
private void cancelExistingConnect()
{
connectCancelSource.Cancel();
connectCancelSource = new CancellationTokenSource();
}
public override string ToString() => $"Connector for {clientName} ({(IsConnected.Value ? "connected" : "not connected")}";
public void Dispose()
{
apiState.UnbindAll();
cancelExistingConnect();
}
protected override string ClientName { get; }
}
}

View File

@ -0,0 +1,35 @@
// 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.Threading;
using System.Threading.Tasks;
namespace osu.Game.Online
{
public abstract class PersistentEndpointClient : IAsyncDisposable
{
/// <summary>
/// An event notifying the <see cref="PersistentEndpointClientConnector"/> that the connection has been closed
/// </summary>
public event Func<Exception?, Task>? Closed;
/// <summary>
/// Notifies the <see cref="PersistentEndpointClientConnector"/> that the connection has been closed.
/// </summary>
/// <param name="exception">The exception that the connection closed with.</param>
protected Task InvokeClosed(Exception? exception) => Closed?.Invoke(exception) ?? Task.CompletedTask;
/// <summary>
/// Connects the client to the remote service to begin processing messages.
/// </summary>
/// <param name="cancellationToken">A cancellation token to stop processing messages.</param>
public abstract Task ConnectAsync(CancellationToken cancellationToken);
public virtual ValueTask DisposeAsync()
{
Closed = null;
return new ValueTask(Task.CompletedTask);
}
}
}

View File

@ -0,0 +1,198 @@
// 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.Threading;
using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Logging;
using osu.Game.Online.API;
namespace osu.Game.Online
{
public abstract class PersistentEndpointClientConnector : IDisposable
{
/// <summary>
/// Whether the managed connection is currently connected. When <c>true</c> use <see cref="CurrentConnection"/> to access the connection.
/// </summary>
public IBindable<bool> IsConnected => isConnected;
/// <summary>
/// The current connection opened by this connector.
/// </summary>
public PersistentEndpointClient? CurrentConnection { get; private set; }
private readonly Bindable<bool> isConnected = new Bindable<bool>();
private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1);
private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
/// <summary>
/// Constructs a new <see cref="PersistentEndpointClientConnector"/>.
/// </summary>
/// <param name="api"> An API provider used to react to connection state changes.</param>
protected PersistentEndpointClientConnector(IAPIProvider api)
{
apiState.BindTo(api.State);
apiState.BindValueChanged(_ => Task.Run(connectIfPossible), true);
}
public Task Reconnect()
{
Logger.Log($"{ClientName} reconnecting...", LoggingTarget.Network);
return Task.Run(connectIfPossible);
}
private async Task connectIfPossible()
{
switch (apiState.Value)
{
case APIState.Failing:
case APIState.Offline:
await disconnect(true);
break;
case APIState.Online:
await connect();
break;
}
}
private async Task connect()
{
cancelExistingConnect();
if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck.");
try
{
while (apiState.Value == APIState.Online)
{
// ensure any previous connection was disposed.
// this will also create a new cancellation token source.
await disconnect(false).ConfigureAwait(false);
// this token will be valid for the scope of this connection.
// if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere.
var cancellationToken = connectCancelSource.Token;
cancellationToken.ThrowIfCancellationRequested();
Logger.Log($"{ClientName} connecting...", LoggingTarget.Network);
try
{
// importantly, rebuild the connection each attempt to get an updated access token.
CurrentConnection = await BuildConnectionAsync(cancellationToken).ConfigureAwait(false);
CurrentConnection.Closed += ex => onConnectionClosed(ex, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
await CurrentConnection.ConnectAsync(cancellationToken).ConfigureAwait(false);
Logger.Log($"{ClientName} connected!", LoggingTarget.Network);
isConnected.Value = true;
return;
}
catch (OperationCanceledException)
{
//connection process was cancelled.
throw;
}
catch (Exception e)
{
await handleErrorAndDelay(e, cancellationToken).ConfigureAwait(false);
}
}
}
finally
{
connectionLock.Release();
}
}
/// <summary>
/// Handles an exception and delays an async flow.
/// </summary>
private async Task handleErrorAndDelay(Exception exception, CancellationToken cancellationToken)
{
Logger.Log($"{ClientName} connect attempt failed: {exception.Message}", LoggingTarget.Network);
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Creates a new <see cref="PersistentEndpointClient"/>.
/// </summary>
/// <param name="cancellationToken">A cancellation token to stop the process.</param>
protected abstract Task<PersistentEndpointClient> BuildConnectionAsync(CancellationToken cancellationToken);
private async Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken)
{
isConnected.Value = false;
if (ex != null)
await handleErrorAndDelay(ex, cancellationToken).ConfigureAwait(false);
else
Logger.Log($"{ClientName} disconnected", LoggingTarget.Network);
// make sure a disconnect wasn't triggered (and this is still the active connection).
if (!cancellationToken.IsCancellationRequested)
await Task.Run(connect, default).ConfigureAwait(false);
}
private async Task disconnect(bool takeLock)
{
cancelExistingConnect();
if (takeLock)
{
if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck.");
}
try
{
if (CurrentConnection != null)
await CurrentConnection.DisposeAsync().ConfigureAwait(false);
}
finally
{
CurrentConnection = null;
if (takeLock)
connectionLock.Release();
}
}
private void cancelExistingConnect()
{
connectCancelSource.Cancel();
connectCancelSource = new CancellationTokenSource();
}
protected virtual string ClientName => GetType().ReadableName();
public override string ToString() => $"{ClientName} ({(IsConnected.Value ? "connected" : "not connected")})";
private bool isDisposed;
protected virtual void Dispose(bool isDisposing)
{
if (isDisposed)
return;
apiState.UnbindAll();
cancelExistingConnect();
isDisposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

View File

@ -179,6 +179,8 @@ namespace osu.Game
private Bindable<string> configRuleset;
private Bindable<bool> applySafeAreaConsiderations;
private Bindable<float> uiScale;
private Bindable<string> configSkin;
@ -280,10 +282,7 @@ namespace osu.Game
configRuleset = LocalConfig.GetBindable<string>(OsuSetting.Ruleset);
uiScale = LocalConfig.GetBindable<float>(OsuSetting.UIScale);
var preferredRuleset = int.TryParse(configRuleset.Value, out int rulesetId)
// int parsing can be removed 20220522
? RulesetStore.GetRuleset(rulesetId)
: RulesetStore.GetRuleset(configRuleset.Value);
var preferredRuleset = RulesetStore.GetRuleset(configRuleset.Value);
try
{
@ -312,6 +311,9 @@ namespace osu.Game
SelectedMods.BindValueChanged(modsChanged);
Beatmap.BindValueChanged(beatmapChanged, true);
applySafeAreaConsiderations = LocalConfig.GetBindable<bool>(OsuSetting.SafeAreaConsiderations);
applySafeAreaConsiderations.BindValueChanged(apply => SafeAreaContainer.SafeAreaOverrideEdges = apply.NewValue ? SafeAreaOverrideEdges : Edges.All, true);
}
private ExternalLinkOpener externalLinkOpener;

View File

@ -21,7 +21,11 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Input.Handlers;
using osu.Framework.Input.Handlers.Joystick;
using osu.Framework.Input.Handlers.Midi;
using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Input.Handlers.Touch;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Framework.Platform;
@ -46,6 +50,7 @@ using osu.Game.Online.Spectator;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections;
using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Resources;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@ -189,6 +194,8 @@ namespace osu.Game
private RealmAccess realm;
protected SafeAreaContainer SafeAreaContainer { get; private set; }
/// <summary>
/// For now, this is used as a source specifically for beat synced components.
/// Going forward, it could potentially be used as the single source-of-truth for beatmap timing.
@ -341,7 +348,7 @@ namespace osu.Game
GlobalActionContainer globalBindings;
base.Content.Add(new SafeAreaContainer
base.Content.Add(SafeAreaContainer = new SafeAreaContainer
{
SafeAreaOverrideEdges = SafeAreaOverrideEdges,
RelativeSizeAxes = Axes.Both,
@ -521,6 +528,29 @@ namespace osu.Game
/// <remarks>Should be overriden per-platform to provide settings for platform-specific handlers.</remarks>
public virtual SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
{
// One would think that this could be moved to the `OsuGameDesktop` class, but doing so means that
// OsuGameTestScenes will not show any input options (as they are based on OsuGame not OsuGameDesktop).
//
// This in turn makes it hard for ruleset creators to adjust input settings while testing their ruleset
// within the test browser interface.
if (RuntimeInfo.IsDesktop)
{
switch (handler)
{
case ITabletHandler th:
return new TabletSettings(th);
case MouseHandler mh:
return new MouseSettings(mh);
case JoystickHandler jh:
return new JoystickSettings(jh);
case TouchHandler:
return new InputSection.HandlerSection(handler);
}
}
switch (handler)
{
case MidiHandler:

View File

@ -115,6 +115,7 @@ namespace osu.Game.Overlays
{
filterControl.Search(query);
Show();
ScrollFlow.ScrollToStart();
}
protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader();

View File

@ -148,7 +148,7 @@ namespace osu.Game.Overlays.Notifications
}
}
private bool completionSent;
private int completionSent;
/// <summary>
/// Attempt to post a completion notification.
@ -162,11 +162,11 @@ namespace osu.Game.Overlays.Notifications
if (CompletionTarget == null)
return;
if (completionSent)
// Thread-safe barrier, as this may be called by a web request and also scheduled to the update thread at the same time.
if (Interlocked.Exchange(ref completionSent, 1) == 1)
return;
CompletionTarget.Invoke(CreateCompletionNotification());
completionSent = true;
Close(false);
}

View File

@ -20,6 +20,7 @@ using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Settings.Sections.Graphics
@ -50,6 +51,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private SettingsDropdown<Size> resolutionDropdown = null!;
private SettingsDropdown<Display> displayDropdown = null!;
private SettingsDropdown<WindowMode> windowModeDropdown = null!;
private SettingsCheckbox safeAreaConsiderationsCheckbox = null!;
private Bindable<float> scalingPositionX = null!;
private Bindable<float> scalingPositionY = null!;
@ -101,6 +103,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
ItemSource = resolutions,
Current = sizeFullscreen
},
safeAreaConsiderationsCheckbox = new SettingsCheckbox
{
LabelText = "Shrink game to avoid cameras and notches",
Current = osuConfig.GetBindable<bool>(OsuSetting.SafeAreaConsiderations),
},
new SettingsSlider<float, UIScaleSlider>
{
LabelText = GraphicsSettingsStrings.UIScaling,
@ -166,7 +173,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
windowModeDropdown.Current.BindValueChanged(_ =>
{
updateDisplayModeDropdowns();
updateDisplaySettingsVisibility();
updateScreenModeWarning();
}, true);
@ -191,7 +198,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
.Distinct());
}
updateDisplayModeDropdowns();
updateDisplaySettingsVisibility();
}), true);
scalingMode.BindValueChanged(_ =>
@ -221,11 +228,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
Scheduler.AddOnce(d =>
{
displayDropdown.Items = d;
updateDisplayModeDropdowns();
updateDisplaySettingsVisibility();
}, displays);
}
private void updateDisplayModeDropdowns()
private void updateDisplaySettingsVisibility()
{
if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen)
resolutionDropdown.Show();
@ -236,6 +243,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
displayDropdown.Show();
else
displayDropdown.Hide();
if (host.Window?.SafeAreaPadding.Value.Total != Vector2.Zero)
safeAreaConsiderationsCheckbox.Show();
else
safeAreaConsiderationsCheckbox.Hide();
}
private void updateScreenModeWarning()

View File

@ -141,6 +141,8 @@ namespace osu.Game.Overlays.Toolbar
Name = "Right buttons",
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Children = new Drawable[]
{
new Box

View File

@ -23,10 +23,14 @@ namespace osu.Game.Overlays.Volume
{
case GlobalAction.DecreaseVolume:
case GlobalAction.IncreaseVolume:
ActionRequested?.Invoke(e.Action);
return true;
case GlobalAction.ToggleMute:
case GlobalAction.NextVolumeMeter:
case GlobalAction.PreviousVolumeMeter:
ActionRequested?.Invoke(e.Action);
if (!e.Repeat)
ActionRequested?.Invoke(e.Action);
return true;
}

View File

@ -3,6 +3,9 @@
#nullable disable
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
@ -10,9 +13,11 @@ using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
@ -20,6 +25,7 @@ using osu.Game.Overlays;
using osu.Game.Overlays.OSD;
using osu.Game.Overlays.Settings.Sections;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Components.TernaryButtons;
namespace osu.Game.Rulesets.Edit
{
@ -32,7 +38,7 @@ namespace osu.Game.Rulesets.Edit
{
private const float adjust_step = 0.1f;
public Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1.0)
public BindableDouble DistanceSpacingMultiplier { get; } = new BindableDouble(1.0)
{
MinValue = 0.1,
MaxValue = 6.0,
@ -44,10 +50,15 @@ namespace osu.Game.Rulesets.Edit
protected ExpandingToolboxContainer RightSideToolboxContainer { get; private set; }
private ExpandableSlider<double, SizeSlider<double>> distanceSpacingSlider;
private ExpandableButton currentDistanceSpacingButton;
[Resolved(canBeNull: true)]
private OnScreenDisplay onScreenDisplay { get; set; }
protected readonly Bindable<TernaryState> DistanceSnapToggle = new Bindable<TernaryState>();
private bool distanceSnapMomentary;
protected DistancedHitObjectComposer(Ruleset ruleset)
: base(ruleset)
{
@ -74,10 +85,27 @@ namespace osu.Game.Rulesets.Edit
Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
Child = new EditorToolboxGroup("snapping")
{
Child = distanceSpacingSlider = new ExpandableSlider<double, SizeSlider<double>>
Children = new Drawable[]
{
Current = { BindTarget = DistanceSpacingMultiplier },
KeyboardStep = adjust_step,
distanceSpacingSlider = new ExpandableSlider<double, SizeSlider<double>>
{
KeyboardStep = adjust_step,
// Manual binding in LoadComplete to handle one-way event flow.
Current = DistanceSpacingMultiplier.GetUnboundCopy(),
},
currentDistanceSpacingButton = new ExpandableButton
{
Action = () =>
{
(HitObject before, HitObject after)? objects = getObjectsOnEitherSideOfCurrentTime();
Debug.Assert(objects != null);
DistanceSpacingMultiplier.Value = ReadCurrentDistanceSnap(objects.Value.before, objects.Value.after);
DistanceSnapToggle.Value = TernaryState.True;
},
RelativeSizeAxes = Axes.X,
}
}
}
}
@ -85,6 +113,51 @@ namespace osu.Game.Rulesets.Edit
});
}
private (HitObject before, HitObject after)? getObjectsOnEitherSideOfCurrentTime()
{
HitObject lastBefore = Playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime <= EditorClock.CurrentTime)?.HitObject;
if (lastBefore == null)
return null;
HitObject firstAfter = Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime >= EditorClock.CurrentTime)?.HitObject;
if (firstAfter == null)
return null;
if (lastBefore == firstAfter)
return null;
return (lastBefore, firstAfter);
}
protected abstract double ReadCurrentDistanceSnap(HitObject before, HitObject after);
protected override void Update()
{
base.Update();
(HitObject before, HitObject after)? objects = getObjectsOnEitherSideOfCurrentTime();
double currentSnap = objects == null
? 0
: ReadCurrentDistanceSnap(objects.Value.before, objects.Value.after);
if (currentSnap > DistanceSpacingMultiplier.MinValue)
{
currentDistanceSpacingButton.Enabled.Value = currentDistanceSpacingButton.Expanded.Value
&& !Precision.AlmostEquals(currentSnap, DistanceSpacingMultiplier.Value, DistanceSpacingMultiplier.Precision / 2);
currentDistanceSpacingButton.ContractedLabelText = $"current {currentSnap:N2}x";
currentDistanceSpacingButton.ExpandedLabelText = $"Use current ({currentSnap:N2}x)";
}
else
{
currentDistanceSpacingButton.Enabled.Value = false;
currentDistanceSpacingButton.ContractedLabelText = string.Empty;
currentDistanceSpacingButton.ExpandedLabelText = "Use current (unavailable)";
}
}
protected override void LoadComplete()
{
base.LoadComplete();
@ -102,6 +175,45 @@ namespace osu.Game.Rulesets.Edit
EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
}, true);
// Manual binding to handle enabling distance spacing when the slider is interacted with.
distanceSpacingSlider.Current.BindValueChanged(spacing =>
{
DistanceSpacingMultiplier.Value = spacing.NewValue;
DistanceSnapToggle.Value = TernaryState.True;
});
DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue);
}
}
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
{
new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
});
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat)
return false;
handleToggleViaKey(e);
return base.OnKeyDown(e);
}
protected override void OnKeyUp(KeyUpEvent e)
{
handleToggleViaKey(e);
base.OnKeyUp(e);
}
private void handleToggleViaKey(KeyboardEvent key)
{
bool altPressed = key.AltPressed;
if (altPressed != distanceSnapMomentary)
{
distanceSnapMomentary = altPressed;
DistanceSnapToggle.Value = DistanceSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False;
}
}
@ -111,7 +223,7 @@ namespace osu.Game.Rulesets.Edit
{
case GlobalAction.EditorIncreaseDistanceSpacing:
case GlobalAction.EditorDecreaseDistanceSpacing:
return adjustDistanceSpacing(e.Action, adjust_step);
return AdjustDistanceSpacing(e.Action, adjust_step);
}
return false;
@ -127,13 +239,13 @@ namespace osu.Game.Rulesets.Edit
{
case GlobalAction.EditorIncreaseDistanceSpacing:
case GlobalAction.EditorDecreaseDistanceSpacing:
return adjustDistanceSpacing(e.Action, e.ScrollAmount * adjust_step);
return AdjustDistanceSpacing(e.Action, e.ScrollAmount * adjust_step);
}
return false;
}
private bool adjustDistanceSpacing(GlobalAction action, float amount)
protected virtual bool AdjustDistanceSpacing(GlobalAction action, float amount)
{
if (DistanceSpacingMultiplier.Disabled)
return false;
@ -143,12 +255,13 @@ namespace osu.Game.Rulesets.Edit
else if (action == GlobalAction.EditorDecreaseDistanceSpacing)
DistanceSpacingMultiplier.Value -= amount;
DistanceSnapToggle.Value = TernaryState.True;
return true;
}
public virtual float GetBeatSnapDistanceAt(HitObject referenceObject)
public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true)
{
return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * 1 / BeatSnapProvider.BeatDivisor);
return (float)(100 * (useReferenceSliderVelocity ? referenceObject.DifficultyControlPoint.SliderVelocity : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1 / BeatSnapProvider.BeatDivisor);
}
public virtual float DurationToDistance(HitObject referenceObject, double duration)

View File

@ -0,0 +1,101 @@
// 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.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Rulesets.Edit
{
internal class ExpandableButton : RoundedButton, IExpandable
{
private float actualHeight;
public override float Height
{
get => base.Height;
set => base.Height = actualHeight = value;
}
private LocalisableString contractedLabelText;
/// <summary>
/// The label text to display when this button is in a contracted state.
/// </summary>
public LocalisableString ContractedLabelText
{
get => contractedLabelText;
set
{
if (value == contractedLabelText)
return;
contractedLabelText = value;
if (!Expanded.Value)
Text = value;
}
}
private LocalisableString expandedLabelText;
/// <summary>
/// The label text to display when this button is in an expanded state.
/// </summary>
public LocalisableString ExpandedLabelText
{
get => expandedLabelText;
set
{
if (value == expandedLabelText)
return;
expandedLabelText = value;
if (Expanded.Value)
Text = value;
}
}
public BindableBool Expanded { get; } = new BindableBool();
[Resolved(canBeNull: true)]
private IExpandingContainer? expandingContainer { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
expandingContainer?.Expanded.BindValueChanged(containerExpanded =>
{
Expanded.Value = containerExpanded.NewValue;
}, true);
Expanded.BindValueChanged(expanded =>
{
Text = expanded.NewValue ? expandedLabelText : contractedLabelText;
if (expanded.NewValue)
{
SpriteText.Anchor = Anchor.Centre;
SpriteText.Origin = Anchor.Centre;
SpriteText.Font = OsuFont.GetFont(weight: FontWeight.Bold);
base.Height = actualHeight;
Background.Show();
}
else
{
SpriteText.Anchor = Anchor.CentreLeft;
SpriteText.Origin = Anchor.CentreLeft;
SpriteText.Font = OsuFont.GetFont(weight: FontWeight.Regular);
base.Height = actualHeight / 2;
Background.Hide();
}
}, true);
}
}
}

View File

@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Edit
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.ControlPressed || e.AltPressed || e.SuperPressed)
if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.ShiftPressed)
return false;
if (checkLeftToggleFromKey(e.Key, out int leftIndex))

View File

@ -27,8 +27,9 @@ namespace osu.Game.Rulesets.Edit
/// Retrieves the distance between two points within a timing point that are one beat length apart.
/// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
/// <param name="useReferenceSliderVelocity">Whether the <paramref name="referenceObject"/>'s slider velocity should be factored into the returned distance.</param>
/// <returns>The distance between two points residing in the timing point that are one beat length apart.</returns>
float GetBeatSnapDistanceAt(HitObject referenceObject);
float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true);
/// <summary>
/// Converts a duration to a distance without applying any snapping.

View File

@ -1,18 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mods
{
[Obsolete(@"Use the singular version IApplicableToDrawableHitObject instead.")] // Can be removed 20211216
public interface IApplicableToDrawableHitObjects : IApplicableToDrawableHitObject
{
void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables);
void IApplicableToDrawableHitObject.ApplyToDrawableHitObject(DrawableHitObject drawable) => ApplyToDrawableHitObjects(drawable.Yield());
}
}

View File

@ -1,22 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods
{
[Obsolete("Use ICreateReplayData instead")] // Can be removed 20220929
public interface ICreateReplay : ICreateReplayData
{
public Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods);
ModReplayData ICreateReplayData.CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
{
var replayScore = CreateReplayScore(beatmap, mods);
return new ModReplayData(replayScore.Replay, new ModCreatedUser { Username = replayScore.ScoreInfo.User.Username });
}
}
}

View File

@ -101,9 +101,6 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore]
public virtual bool ValidForMultiplayerAsFreeMod => true;
[Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to false.")] // Can be removed 20211009
public virtual bool Ranked => false;
/// <summary>
/// Whether this mod requires configuration to apply changes to the game.
/// </summary>

View File

@ -8,7 +8,6 @@ using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Replays;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods
{
@ -33,16 +32,6 @@ namespace osu.Game.Rulesets.Mods
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
[Obsolete("Override CreateReplayData(IBeatmap, IReadOnlyList<Mod>) instead")] // Can be removed 20220929
public virtual Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score { Replay = new Replay() };
public virtual ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
{
#pragma warning disable CS0618
var replayScore = CreateReplayScore(beatmap, mods);
#pragma warning restore CS0618
return new ModReplayData(replayScore.Replay, new ModCreatedUser { Username = replayScore.ScoreInfo.User.Username });
}
public virtual ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new ModReplayData(new Replay(), new ModCreatedUser { Username = @"autoplay" });
}
}

View File

@ -196,18 +196,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
updateState(State.Value, true);
}
/// <summary>
/// Applies a hit object to be represented by this <see cref="DrawableHitObject"/>.
/// </summary>
[Obsolete("Use either overload of Apply that takes a single argument of type HitObject or HitObjectLifetimeEntry")] // Can be removed 20211021.
public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry)
{
if (lifetimeEntry != null)
Apply(lifetimeEntry);
else
Apply(hitObject);
}
/// <summary>
/// Applies a new <see cref="HitObject"/> to be represented by this <see cref="DrawableHitObject"/>.
/// A new <see cref="HitObjectLifetimeEntry"/> is automatically created and applied to this <see cref="DrawableHitObject"/>.
@ -278,6 +266,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
updateState(ArmedState.Miss, true);
else
updateState(ArmedState.Idle, true);
// Combo colour may have been applied via a bindable flow while no object entry was attached.
// Update here to ensure we're in a good state.
UpdateComboColour();
}
}

View File

@ -199,8 +199,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (stringAddBank == @"none")
stringAddBank = null;
bankInfo.Normal = stringBank;
bankInfo.Add = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank;
bankInfo.BankForNormal = stringBank;
bankInfo.BankForAdditions = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank;
if (split.Length > 2)
bankInfo.CustomSampleBank = Parsing.ParseInt(split[2]);
@ -447,32 +447,54 @@ namespace osu.Game.Rulesets.Objects.Legacy
var soundTypes = new List<HitSampleInfo>
{
new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.Normal, bankInfo.Volume, bankInfo.CustomSampleBank,
new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, bankInfo.CustomSampleBank,
// if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
// None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal))
};
if (type.HasFlagFast(LegacyHitSoundType.Finish))
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank));
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
if (type.HasFlagFast(LegacyHitSoundType.Whistle))
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank));
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
if (type.HasFlagFast(LegacyHitSoundType.Clap))
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank));
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
return soundTypes;
}
private class SampleBankInfo
{
/// <summary>
/// An optional overriding filename which causes all bank/sample specifications to be ignored.
/// </summary>
public string Filename;
public string Normal;
public string Add;
/// <summary>
/// The bank identifier to use for the base ("hitnormal") sample.
/// Transferred to <see cref="HitSampleInfo.Bank"/> when appropriate.
/// </summary>
public string BankForNormal;
/// <summary>
/// The bank identifier to use for additions ("hitwhistle", "hitfinish", "hitclap").
/// Transferred to <see cref="HitSampleInfo.Bank"/> when appropriate.
/// </summary>
public string BankForAdditions;
/// <summary>
/// Hit sample volume (0-100).
/// See <see cref="HitSampleInfo.Volume"/>.
/// </summary>
public int Volume;
/// <summary>
/// The index of the custom sample bank. Is only used if 2 or above for "reasons".
/// This will add a suffix to lookups, allowing extended bank lookups (ie. "normal-hitnormal-2").
/// See <see cref="HitSampleInfo.Suffix"/>.
/// </summary>
public int CustomSampleBank;
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
@ -503,7 +525,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string?> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default)
=> With(newName, newBank, newVolume);
public virtual LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string?> newBank = default, Optional<int> newVolume = default, Optional<int> newCustomSampleBank = default,
public virtual LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string?> newBank = default, Optional<int> newVolume = default,
Optional<int> newCustomSampleBank = default,
Optional<bool> newIsLayered = default)
=> new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered));
@ -537,7 +560,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
Path.ChangeExtension(Filename, null)
};
public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string?> newBank = default, Optional<int> newVolume = default, Optional<int> newCustomSampleBank = default,
public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string?> newBank = default, Optional<int> newVolume = default,
Optional<int> newCustomSampleBank = default,
Optional<bool> newIsLayered = default)
=> new FileHitSampleInfo(Filename, newVolume.GetOr(Volume));

View File

@ -158,7 +158,7 @@ namespace osu.Game.Rulesets
}
catch (Exception e)
{
LogFailedLoad(assembly.FullName, e);
LogFailedLoad(assembly.GetName().Name.Split('.').Last(), e);
}
}
@ -168,14 +168,14 @@ namespace osu.Game.Rulesets
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
protected void Dispose(bool disposing)
{
AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly;
}
protected void LogFailedLoad(string name, Exception exception)
{
Logger.Log($"Could not load ruleset {name}. Please check for an update from the developer.", level: LogLevel.Error);
Logger.Log($"Could not load ruleset \"{name}\". Please check for an update from the developer.", level: LogLevel.Error);
Logger.Log($"Ruleset load failed: {exception}");
}

View File

@ -25,6 +25,26 @@ namespace osu.Game.Screens.Edit
BindValueChanged(_ => ensureValidDivisor());
}
/// <summary>
/// Set a divisor, updating the valid divisor range appropriately.
/// </summary>
/// <param name="divisor">The intended divisor.</param>
public void SetArbitraryDivisor(int divisor)
{
// If the current valid divisor range doesn't contain the proposed value, attempt to find one which does.
if (!ValidDivisors.Value.Presets.Contains(divisor))
{
if (BeatDivisorPresetCollection.COMMON.Presets.Contains(divisor))
ValidDivisors.Value = BeatDivisorPresetCollection.COMMON;
else if (BeatDivisorPresetCollection.TRIPLETS.Presets.Contains(divisor))
ValidDivisors.Value = BeatDivisorPresetCollection.TRIPLETS;
else
ValidDivisors.Value = BeatDivisorPresetCollection.Custom(divisor);
}
Value = divisor;
}
private void updateBindableProperties()
{
ensureValidDivisor();

View File

@ -1,28 +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 osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Screens.Edit.Components
{
public class CircularButton : OsuButton
{
private const float width = 125;
private const float height = 30;
public CircularButton()
{
Size = new Vector2(width, height);
}
protected override void Update()
{
base.Update();
Content.CornerRadius = DrawHeight / 2f;
Content.CornerExponent = 2;
}
}
}

View File

@ -209,6 +209,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.ShiftPressed && e.Key >= Key.Number1 && e.Key <= Key.Number9)
{
beatDivisor.SetArbitraryDivisor(e.Key - Key.Number0);
return true;
}
return base.OnKeyDown(e);
}
internal class DivisorDisplay : OsuAnimatedButton, IHasPopover
{
public BindableBeatDivisor BeatDivisor { get; } = new BindableBeatDivisor();
@ -306,17 +317,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
return;
}
if (!BeatDivisor.ValidDivisors.Value.Presets.Contains(divisor))
{
if (BeatDivisorPresetCollection.COMMON.Presets.Contains(divisor))
BeatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.COMMON;
else if (BeatDivisorPresetCollection.TRIPLETS.Presets.Contains(divisor))
BeatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.TRIPLETS;
else
BeatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.Custom(divisor);
}
BeatDivisor.Value = divisor;
BeatDivisor.SetArbitraryDivisor(divisor);
this.HidePopover();
}

View File

@ -53,9 +53,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
float maxDistance = new Vector2(dx, dy).Length;
int requiredCircles = Math.Min(MaxIntervals, (int)(maxDistance / DistanceBetweenTicks));
// We need to offset the drawn lines to the next valid snap for the currently selected divisor.
//
// Picture the scenario where the user has just placed an object on a 1/2 snap, then changes to
// 1/3 snap and expects to be able to place the next object on a valid 1/3 snap, regardless of the
// fact that the 1/2 snap reference object is not valid for 1/3 snapping.
float offset = SnapProvider.FindSnappedDistance(ReferenceObject, 0);
for (int i = 0; i < requiredCircles; i++)
{
float diameter = (i + 1) * DistanceBetweenTicks * 2;
float diameter = (offset + (i + 1) * DistanceBetweenTicks) * 2;
AddInternal(new Ring(ReferenceObject, GetColourForIndexFromPlacement(i))
{

View File

@ -97,7 +97,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updateSpacing()
{
float distanceSpacingMultiplier = (float)DistanceSpacingMultiplier.Value;
float beatSnapDistance = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject);
float beatSnapDistance = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject, false);
DistanceBetweenTicks = beatSnapDistance * distanceSpacingMultiplier;

View File

@ -250,7 +250,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void seekTrackToCurrent()
{
double target = Current / Content.DrawWidth * editorClock.TrackLength;
double target = TimeAtPosition(Current);
editorClock.Seek(Math.Min(editorClock.TrackLength, target));
}
@ -264,7 +264,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (handlingDragInput)
editorClock.Stop();
ScrollTo((float)(editorClock.CurrentTime / editorClock.TrackLength) * Content.DrawWidth, false);
float position = PositionAtTime(editorClock.CurrentTime);
ScrollTo(position, false);
}
protected override bool OnMouseDown(MouseDownEvent e)

View File

@ -307,6 +307,7 @@ namespace osu.Game.Screens.Edit
cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut),
copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy),
pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste),
cloneMenuItem = new EditorMenuItem("Clone", MenuItemType.Standard, Clone),
}
},
new MenuItem("View")
@ -581,6 +582,10 @@ namespace osu.Game.Screens.Edit
this.Exit();
return true;
case GlobalAction.EditorCloneSelection:
Clone();
return true;
case GlobalAction.EditorComposeMode:
Mode.Value = EditorScreenMode.Compose;
return true;
@ -749,6 +754,7 @@ namespace osu.Game.Screens.Edit
private EditorMenuItem cutMenuItem;
private EditorMenuItem copyMenuItem;
private EditorMenuItem cloneMenuItem;
private EditorMenuItem pasteMenuItem;
private readonly BindableWithCurrent<bool> canCut = new BindableWithCurrent<bool>();
@ -758,7 +764,11 @@ namespace osu.Game.Screens.Edit
private void setUpClipboardActionAvailability()
{
canCut.Current.BindValueChanged(cut => cutMenuItem.Action.Disabled = !cut.NewValue, true);
canCopy.Current.BindValueChanged(copy => copyMenuItem.Action.Disabled = !copy.NewValue, true);
canCopy.Current.BindValueChanged(copy =>
{
copyMenuItem.Action.Disabled = !copy.NewValue;
cloneMenuItem.Action.Disabled = !copy.NewValue;
}, true);
canPaste.Current.BindValueChanged(paste => pasteMenuItem.Action.Disabled = !paste.NewValue, true);
}
@ -773,6 +783,21 @@ namespace osu.Game.Screens.Edit
protected void Copy() => currentScreen?.Copy();
protected void Clone()
{
// Avoid attempting to clone if copying is not available (as it may result in pasting something unexpected).
if (!canCopy.Value)
return;
// This is an initial implementation just to get an idea of how people used this function.
// There are a couple of differences from osu!stable's implementation which will require more work to match:
// - The "clipboard" is not populated during the duplication process.
// - The duplicated hitobjects are inserted after the original pattern (add one beat_length and then quantize using beat snap).
// - The duplicated hitobjects are selected (but this is also applied for all paste operations so should be changed there).
Copy();
Paste();
}
protected void Paste() => currentScreen?.Paste();
#endregion

View File

@ -113,7 +113,7 @@ namespace osu.Game.Screens.Menu
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
logoBounceContainer = new DragContainer
logoBounceContainer = new Container
{
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
@ -407,27 +407,24 @@ namespace osu.Game.Screens.Menu
impactContainer.ScaleTo(1.12f, 250);
}
private class DragContainer : Container
public override bool DragBlocksClick => false;
protected override bool OnDragStart(DragStartEvent e) => true;
protected override void OnDrag(DragEvent e)
{
public override bool DragBlocksClick => false;
Vector2 change = e.MousePosition - e.MouseDownPosition;
protected override bool OnDragStart(DragStartEvent e) => true;
// Diminish the drag distance as we go further to simulate "rubber band" feeling.
change *= change.Length <= 0 ? 0 : MathF.Pow(change.Length, 0.6f) / change.Length;
protected override void OnDrag(DragEvent e)
{
Vector2 change = e.MousePosition - e.MouseDownPosition;
logoBounceContainer.MoveTo(change);
}
// Diminish the drag distance as we go further to simulate "rubber band" feeling.
change *= change.Length <= 0 ? 0 : MathF.Pow(change.Length, 0.6f) / change.Length;
this.MoveTo(change);
}
protected override void OnDragEnd(DragEndEvent e)
{
this.MoveTo(Vector2.Zero, 800, Easing.OutElastic);
base.OnDragEnd(e);
}
protected override void OnDragEnd(DragEndEvent e)
{
logoBounceContainer.MoveTo(Vector2.Zero, 800, Easing.OutElastic);
base.OnDragEnd(e);
}
}
}

View File

@ -9,7 +9,6 @@ using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Play.HUD
@ -45,12 +44,6 @@ namespace osu.Game.Screens.Play.HUD
[Resolved]
private DrawableRuleset? drawableRuleset { get; set; }
[Resolved]
private OsuConfigManager config { get; set; } = null!;
[Resolved]
private SkinManager skinManager { get; set; } = null!;
public DefaultSongProgress()
{
RelativeSizeAxes = Axes.X;
@ -100,47 +93,6 @@ namespace osu.Game.Screens.Play.HUD
{
AllowSeeking.BindValueChanged(_ => updateBarVisibility(), true);
ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true);
migrateSettingFromConfig();
}
/// <summary>
/// This setting has been migrated to a per-component level.
/// Only take the value from the config if it is in a non-default state (then reset it to default so it only applies once).
///
/// Can be removed 20221027.
/// </summary>
private void migrateSettingFromConfig()
{
Bindable<bool> configShowGraph = config.GetBindable<bool>(OsuSetting.ShowProgressGraph);
if (!configShowGraph.IsDefault)
{
ShowGraph.Value = configShowGraph.Value;
// This is pretty ugly, but the only way to make this stick...
var skinnableTarget = this.FindClosestParent<ISkinnableTarget>();
if (skinnableTarget != null)
{
// If the skin is not mutable, a mutable instance will be created, causing this migration logic to run again on the correct skin.
// Therefore we want to avoid resetting the config value on this invocation.
if (skinManager.EnsureMutableSkin())
return;
// If `EnsureMutableSkin` actually changed the skin, default layout may take a frame to apply.
// See `SkinnableTargetComponentsContainer`'s use of ScheduleAfterChildren.
ScheduleAfterChildren(() =>
{
var skin = skinManager.CurrentSkin.Value;
skin.UpdateDrawableTarget(skinnableTarget);
skinManager.Save(skin);
});
configShowGraph.SetDefault();
}
}
}
protected override void PopIn()

View File

@ -279,6 +279,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
switch (style)
{
case LabelStyles.None:
labelEarly.Clear();
labelLate.Clear();
break;
case LabelStyles.Icons:

View File

@ -39,9 +39,16 @@ namespace osu.Game.Screens.Play
/// </summary>
public float BottomScoringElementsHeight { get; private set; }
// HUD uses AlwaysVisible on child components so they can be in an updated state for next display.
// Without blocking input, this would also allow them to be interacted with in such a state.
public override bool PropagatePositionalInputSubTree => ShowHud.Value;
protected override bool ShouldBeConsideredForInput(Drawable child)
{
// HUD uses AlwaysVisible on child components so they can be in an updated state for next display.
// Without blocking input, this would also allow them to be interacted with in such a state.
if (ShowHud.Value)
return base.ShouldBeConsideredForInput(child);
// hold to quit button should always be interactive.
return child == bottomRightElements;
}
public readonly KeyCounterDisplay KeyCounter;
public readonly ModDisplay ModDisplay;

View File

@ -1,22 +1,27 @@
// 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.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Play.PlayerSettings
{
public class PlayerCheckbox : OsuCheckbox
public class PlayerCheckbox : SettingsCheckbox
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
protected override Drawable CreateControl() => new PlayerCheckboxControl();
public class PlayerCheckboxControl : OsuCheckbox
{
Nub.AccentColour = colours.Yellow;
Nub.GlowingAccentColour = colours.YellowLighter;
Nub.GlowColour = colours.YellowDark;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Nub.AccentColour = colours.Yellow;
Nub.GlowingAccentColour = colours.YellowLighter;
Nub.GlowColour = colours.YellowDark;
}
}
}
}

View File

@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
namespace osu.Game.Screens.Play.PlayerSettings
@ -24,26 +21,16 @@ namespace osu.Game.Screens.Play.PlayerSettings
{
Children = new Drawable[]
{
new OsuSpriteText
{
Text = GameplaySettingsStrings.BackgroundDim
},
dimSliderBar = new PlayerSliderBar<double>
{
LabelText = GameplaySettingsStrings.BackgroundDim,
DisplayAsPercentage = true
},
new OsuSpriteText
{
Text = GameplaySettingsStrings.BackgroundBlur
},
blurSliderBar = new PlayerSliderBar<double>
{
LabelText = GameplaySettingsStrings.BackgroundBlur,
DisplayAsPercentage = true
},
new OsuSpriteText
{
Text = "Toggles:"
},
showStoryboardToggle = new PlayerCheckbox { LabelText = GraphicsSettingsStrings.StoryboardVideo },
beatmapSkinsToggle = new PlayerCheckbox { LabelText = SkinSettingsStrings.BeatmapSkins },
beatmapColorsToggle = new PlayerCheckbox { LabelText = SkinSettingsStrings.BeatmapColours },

View File

@ -36,12 +36,6 @@ namespace osu.Game.Screens.Ranking.Statistics
/// </summary>
public readonly bool RequiresHitEvents;
[Obsolete("Use constructor which takes creation function instead.")] // Can be removed 20220803.
public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null)
: this(name, () => content, true, dimension)
{
}
/// <summary>
/// Creates a new <see cref="StatisticItem"/>, to be displayed inside a <see cref="StatisticRow"/> in the results screen.
/// </summary>

View File

@ -1048,7 +1048,7 @@ namespace osu.Game.Screens.Select
protected override void PerformSelection()
{
if (LastSelected == null || LastSelected.Filtered.Value)
if (LastSelected == null)
carousel?.SelectNextRandom();
else
base.PerformSelection();

View File

@ -108,10 +108,35 @@ namespace osu.Game.Screens.Select.Carousel
PerformSelection();
}
/// <summary>
/// Finds the item this group would select next if it attempted selection
/// </summary>
/// <returns>An unfiltered item nearest to the last selected one or null if all items are filtered</returns>
protected virtual CarouselItem GetNextToSelect()
{
return Items.Skip(lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value) ??
Items.Reverse().Skip(Items.Count - lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value);
if (Items.Count == 0)
return null;
int forwardsIndex = lastSelectedIndex;
int backwardsIndex = Math.Min(lastSelectedIndex, Items.Count - 1);
while (true)
{
bool hasBackwards = backwardsIndex >= 0 && backwardsIndex < Items.Count;
bool hasForwards = forwardsIndex < Items.Count;
if (!hasBackwards && !hasForwards)
return null;
if (hasForwards && !Items[forwardsIndex].Filtered.Value)
return Items[forwardsIndex];
if (hasBackwards && !Items[backwardsIndex].Filtered.Value)
return Items[backwardsIndex];
forwardsIndex++;
backwardsIndex--;
}
}
protected virtual void PerformSelection()

View File

@ -66,8 +66,6 @@ namespace osu.Game.Skinning
}
}
void IHasComboColours.AddComboColours(params Color4[] colours) => CustomComboColours.AddRange(colours);
public Dictionary<string, Color4> CustomColours { get; } = new Dictionary<string, Color4>();
public readonly Dictionary<string, string> ConfigDictionary = new Dictionary<string, string>();

View File

@ -4,11 +4,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using Newtonsoft.Json;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Database;
@ -33,9 +31,6 @@ namespace osu.Game.Skinning
this.skinResources = skinResources;
modelManager = new ModelManager<SkinInfo>(storage, realm);
// can be removed 20220420.
populateMissingHashes();
}
public override IEnumerable<string> HandledExtensions => new[] { ".osk" };
@ -158,18 +153,6 @@ namespace osu.Game.Skinning
}
modelManager.ReplaceFile(existingFile, stream, realm);
// can be removed 20220502.
if (!ensureIniWasUpdated(item))
{
Logger.Log($"Skin {item}'s skin.ini had issues and has been removed. Please report this and provide the problematic skin.", LoggingTarget.Database, LogLevel.Important);
var existingIni = item.GetFile(@"skin.ini");
if (existingIni != null)
item.Files.Remove(existingIni);
writeNewSkinIni();
}
}
}
@ -194,38 +177,6 @@ namespace osu.Game.Skinning
}
}
private bool ensureIniWasUpdated(SkinInfo item)
{
// This is a final consistency check to ensure that hash computation doesn't enter an infinite loop.
// With other changes to the surrounding code this should never be hit, but until we are 101% sure that there
// are no other cases let's avoid a hard startup crash by bailing and alerting.
var instance = createInstance(item);
return instance.Configuration.SkinInfo.Name == item.Name;
}
private void populateMissingHashes()
{
Realm.Run(realm =>
{
var skinsWithoutHashes = realm.All<SkinInfo>().Where(i => !i.Protected && string.IsNullOrEmpty(i.Hash)).ToArray();
foreach (SkinInfo skin in skinsWithoutHashes)
{
try
{
realm.Write(_ => skin.Hash = ComputeHash(skin));
}
catch (Exception e)
{
modelManager.Delete(skin);
Logger.Error(e, $"Existing skin {skin} has been deleted during hash recomputation due to being invalid");
}
}
});
}
private Skin createInstance(SkinInfo item) => item.CreateInstance(skinResources);
public void Save(Skin skin)

View File

@ -110,6 +110,8 @@ namespace osu.Game.Tests.Visual
public new void Paste() => base.Paste();
public new void Clone() => base.Clone();
public new void SwitchToDifficulty(BeatmapInfo beatmapInfo) => base.SwitchToDifficulty(beatmapInfo);
public new void CreateNewDifficulty(RulesetInfo rulesetInfo) => base.CreateNewDifficulty(rulesetInfo);

View File

@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.17.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.1022.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.1101.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1021.0" />
<PackageReference Include="Sentry" Version="3.22.0" />
<PackageReference Include="SharpCompress" Version="0.32.2" />