mirror of
https://github.com/osukey/osukey.git
synced 2025-08-03 14:46:38 +09:00
Merge branch 'master' into editor-background
This commit is contained in:
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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}";
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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, "_");
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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.
|
||||
|
28
osu.Game/Online/HubClient.cs
Normal file
28
osu.Game/Online/HubClient.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
35
osu.Game/Online/PersistentEndpointClient.cs
Normal file
35
osu.Game/Online/PersistentEndpointClient.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
198
osu.Game/Online/PersistentEndpointClientConnector.cs
Normal file
198
osu.Game/Online/PersistentEndpointClientConnector.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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:
|
||||
|
@ -115,6 +115,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
filterControl.Search(query);
|
||||
Show();
|
||||
ScrollFlow.ScrollToStart();
|
||||
}
|
||||
|
||||
protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader();
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
101
osu.Game/Rulesets/Edit/ExpandableButton.cs
Normal file
101
osu.Game/Rulesets/Edit/ExpandableButton.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
|
@ -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.
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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" });
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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}");
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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))
|
||||
{
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -279,6 +279,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
switch (style)
|
||||
{
|
||||
case LabelStyles.None:
|
||||
labelEarly.Clear();
|
||||
labelLate.Clear();
|
||||
break;
|
||||
|
||||
case LabelStyles.Icons:
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 },
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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()
|
||||
|
@ -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>();
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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" />
|
||||
|
Reference in New Issue
Block a user