mirror of
https://github.com/osukey/osukey.git
synced 2025-08-05 07:33:55 +09:00
Merge branch 'master' into combo-colour-brightness-limit
This commit is contained in:
@ -141,18 +141,9 @@ namespace osu.Game.Beatmaps
|
||||
// Handle collections using permissive difficulty name to track difficulties.
|
||||
foreach (var originalBeatmap in original.Beatmaps)
|
||||
{
|
||||
var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalBeatmap.DifficultyName);
|
||||
|
||||
if (updatedBeatmap == null)
|
||||
continue;
|
||||
|
||||
var collections = realm.All<BeatmapCollection>().AsEnumerable().Where(c => c.BeatmapMD5Hashes.Contains(originalBeatmap.MD5Hash));
|
||||
|
||||
foreach (var c in collections)
|
||||
{
|
||||
c.BeatmapMD5Hashes.Remove(originalBeatmap.MD5Hash);
|
||||
c.BeatmapMD5Hashes.Add(updatedBeatmap.MD5Hash);
|
||||
}
|
||||
updated.Beatmaps
|
||||
.FirstOrDefault(b => b.DifficultyName == originalBeatmap.DifficultyName)?
|
||||
.TransferCollectionReferences(realm, originalBeatmap.MD5Hash);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
@ -213,6 +214,23 @@ namespace osu.Game.Beatmaps
|
||||
return fileHashX == fileHashY;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When updating a beatmap, its hashes will change. Collections currently track beatmaps by hash, so they need to be updated.
|
||||
/// This method will handle updating
|
||||
/// </summary>
|
||||
/// <param name="realm">A realm instance in an active write transaction.</param>
|
||||
/// <param name="previousMD5Hash">The previous MD5 hash of the beatmap before update.</param>
|
||||
public void TransferCollectionReferences(Realm realm, string previousMD5Hash)
|
||||
{
|
||||
var collections = realm.All<BeatmapCollection>().AsEnumerable().Where(c => c.BeatmapMD5Hashes.Contains(previousMD5Hash));
|
||||
|
||||
foreach (var c in collections)
|
||||
{
|
||||
c.BeatmapMD5Hashes.Remove(previousMD5Hash);
|
||||
c.BeatmapMD5Hashes.Add(MD5Hash);
|
||||
}
|
||||
}
|
||||
|
||||
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
|
||||
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
|
||||
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
|
||||
@ -220,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;
|
||||
|
||||
|
@ -311,6 +311,8 @@ namespace osu.Game.Beatmaps
|
||||
if (existingFileInfo != null)
|
||||
DeleteFile(setInfo, existingFileInfo);
|
||||
|
||||
string oldMd5Hash = beatmapInfo.MD5Hash;
|
||||
|
||||
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
|
||||
beatmapInfo.Hash = stream.ComputeSHA2Hash();
|
||||
|
||||
@ -327,6 +329,8 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
setInfo.CopyChangesToRealm(liveBeatmapSet);
|
||||
|
||||
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
|
||||
|
||||
ProcessBeatmap?.Invoke((liveBeatmapSet, false));
|
||||
});
|
||||
}
|
||||
@ -336,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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,11 +9,8 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Beatmaps.ControlPoints
|
||||
{
|
||||
public abstract class ControlPoint : IComparable<ControlPoint>, IDeepCloneable<ControlPoint>, IEquatable<ControlPoint>
|
||||
public abstract class ControlPoint : IComparable<ControlPoint>, IDeepCloneable<ControlPoint>, IEquatable<ControlPoint>, IControlPoint
|
||||
{
|
||||
/// <summary>
|
||||
/// The time at which the control point takes effect.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public double Time { get; set; }
|
||||
|
||||
|
@ -196,8 +196,8 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// <param name="time">The time to find the control point at.</param>
|
||||
/// <param name="fallback">The control point to use when <paramref name="time"/> is before any control points.</param>
|
||||
/// <returns>The active control point at <paramref name="time"/>, or a fallback <see cref="ControlPoint"/> if none found.</returns>
|
||||
protected T BinarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T fallback)
|
||||
where T : ControlPoint
|
||||
public static T BinarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T fallback)
|
||||
where T : class, IControlPoint
|
||||
{
|
||||
return BinarySearch(list, time) ?? fallback;
|
||||
}
|
||||
@ -207,9 +207,9 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
/// <param name="list">The list to search.</param>
|
||||
/// <param name="time">The time to find the control point at.</param>
|
||||
/// <returns>The active control point at <paramref name="time"/>.</returns>
|
||||
protected virtual T BinarySearch<T>(IReadOnlyList<T> list, double time)
|
||||
where T : ControlPoint
|
||||
/// <returns>The active control point at <paramref name="time"/>. Will return <c>null</c> if there are no control points, or if the time is before the first control point.</returns>
|
||||
public static T BinarySearch<T>(IReadOnlyList<T> list, double time)
|
||||
where T : class, IControlPoint
|
||||
{
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
|
13
osu.Game/Beatmaps/ControlPoints/IControlPoint.cs
Normal file
13
osu.Game/Beatmaps/ControlPoints/IControlPoint.cs
Normal file
@ -0,0 +1,13 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Beatmaps.ControlPoints
|
||||
{
|
||||
public interface IControlPoint
|
||||
{
|
||||
/// <summary>
|
||||
/// The time at which the control point takes effect.
|
||||
/// </summary>
|
||||
double Time { get; }
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -355,6 +355,14 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case LegacyEventType.Sprite:
|
||||
// Generally, the background is the first thing defined in a beatmap file.
|
||||
// In some older beatmaps, it is not present and replaced by a storyboard-level background instead.
|
||||
// Allow the first sprite (by file order) to act as the background in such cases.
|
||||
if (string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile))
|
||||
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]);
|
||||
break;
|
||||
|
||||
case LegacyEventType.Background:
|
||||
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
|
||||
break;
|
||||
@ -427,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,
|
||||
@ -440,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);
|
||||
}
|
||||
|
||||
|
@ -134,6 +134,6 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
|
||||
/// </summary>
|
||||
void PrepareTrackForPreview(bool looping);
|
||||
void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -110,7 +110,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
|
||||
|
||||
public void PrepareTrackForPreview(bool looping)
|
||||
public void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0)
|
||||
{
|
||||
Track.Looping = looping;
|
||||
Track.RestartPoint = Metadata.PreviewTime;
|
||||
@ -125,6 +125,8 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
Track.RestartPoint = 0.4f * Track.Length;
|
||||
}
|
||||
|
||||
Track.RestartPoint += offsetFromPreviewPoint;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -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,16 +118,14 @@ 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.8, 0, 1, 0.01);
|
||||
SetDefault(OsuSetting.DimLevel, 0.7, 0, 1, 0.01);
|
||||
SetDefault(OsuSetting.BlurLevel, 0, 0, 1, 0.01);
|
||||
SetDefault(OsuSetting.LightenDuringBreaks, true);
|
||||
|
||||
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);
|
||||
@ -154,6 +152,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);
|
||||
@ -206,14 +205,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()
|
||||
@ -299,14 +295,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,
|
||||
@ -375,6 +368,7 @@ namespace osu.Game.Configuration
|
||||
ShowOnlineExplicitContent,
|
||||
LastProcessedMetadataId,
|
||||
NormaliseComboColourBrightness,
|
||||
ComboColourBrightness
|
||||
SafeAreaConsiderations,
|
||||
ComboColourBrightness,
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Database
|
||||
|
||||
public bool Download(T model, bool minimiseDownloadSize = false) => Download(model, minimiseDownloadSize, null);
|
||||
|
||||
public void DownloadAsUpdate(TModel originalModel) => Download(originalModel, false, originalModel);
|
||||
public void DownloadAsUpdate(TModel originalModel, bool minimiseDownloadSize) => Download(originalModel, minimiseDownloadSize, originalModel);
|
||||
|
||||
protected bool Download(T model, bool minimiseDownloadSize, TModel? originalModel)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -294,15 +294,38 @@ namespace osu.Game.Database
|
||||
// Log output here will be missing a valid hash in non-batch imports.
|
||||
LogForModel(item, $@"Beginning import from {archive?.Name ?? "unknown"}...");
|
||||
|
||||
List<RealmNamedFileUsage> files = new List<RealmNamedFileUsage>();
|
||||
|
||||
if (archive != null)
|
||||
{
|
||||
// Import files to the disk store.
|
||||
// We intentionally delay adding to realm to avoid blocking on a write during disk operations.
|
||||
foreach (var filenames in getShortenedFilenames(archive))
|
||||
{
|
||||
using (Stream s = archive.GetStream(filenames.original))
|
||||
files.Add(new RealmNamedFileUsage(Files.Add(s, realm, false), filenames.shortened));
|
||||
}
|
||||
}
|
||||
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
// Add all files to realm in one go.
|
||||
// This is done ahead of the main transaction to ensure we can correctly cleanup the files, even if the import fails.
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (!file.File.IsManaged)
|
||||
realm.Add(file.File, true);
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
item.Files.AddRange(files);
|
||||
item.Hash = ComputeHash(item);
|
||||
|
||||
// TODO: do we want to make the transaction this local? not 100% sure, will need further investigation.
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
if (archive != null)
|
||||
// TODO: look into rollback of file additions (or delayed commit).
|
||||
item.Files.AddRange(createFileInfos(archive, Files, realm));
|
||||
|
||||
item.Hash = ComputeHash(item);
|
||||
|
||||
// TODO: we may want to run this outside of the transaction.
|
||||
Populate(item, archive, realm, cancellationToken);
|
||||
|
||||
@ -425,16 +448,6 @@ namespace osu.Game.Database
|
||||
{
|
||||
var fileInfos = new List<RealmNamedFileUsage>();
|
||||
|
||||
// import files to manager
|
||||
foreach (var filenames in getShortenedFilenames(reader))
|
||||
{
|
||||
using (Stream s = reader.GetStream(filenames.original))
|
||||
{
|
||||
var item = new RealmNamedFileUsage(files.Add(s, realm), filenames.shortened);
|
||||
fileInfos.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return fileInfos;
|
||||
}
|
||||
|
||||
|
@ -40,8 +40,8 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
/// <param name="data">The file data stream.</param>
|
||||
/// <param name="realm">The realm instance to add to. Should already be in a transaction.</param>
|
||||
/// <returns></returns>
|
||||
public RealmFile Add(Stream data, Realm realm)
|
||||
/// <param name="addToRealm">Whether the <see cref="RealmFile"/> should immediately be added to the underlying realm. If <c>false</c> is provided here, the instance must be manually added.</param>
|
||||
public RealmFile Add(Stream data, Realm realm, bool addToRealm = true)
|
||||
{
|
||||
string hash = data.ComputeSHA2Hash();
|
||||
|
||||
@ -52,7 +52,7 @@ namespace osu.Game.Database
|
||||
if (!checkFileExistsAndMatchesHash(file))
|
||||
copyToStore(file, data);
|
||||
|
||||
if (!file.IsManaged)
|
||||
if (addToRealm && !file.IsManaged)
|
||||
realm.Add(file);
|
||||
|
||||
return file;
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO;
|
||||
@ -15,6 +15,8 @@ namespace osu.Game.Extensions
|
||||
{
|
||||
public static class ModelExtensions
|
||||
{
|
||||
private static readonly Regex invalid_filename_chars = new Regex(@"(?!$)[^A-Za-z0-9_()[\]. \-]", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Get the relative path in osu! storage for this file.
|
||||
/// </summary>
|
||||
@ -137,20 +139,14 @@ namespace osu.Game.Extensions
|
||||
return instance.OnlineID.Equals(other.OnlineID);
|
||||
}
|
||||
|
||||
private static readonly char[] invalid_filename_characters = Path.GetInvalidFileNameChars()
|
||||
// Backslash is added to avoid issues when exporting to zip.
|
||||
// See SharpCompress filename normalisation https://github.com/adamhathcock/sharpcompress/blob/a1e7c0068db814c9aa78d86a94ccd1c761af74bd/src/SharpCompress/Writers/Zip/ZipWriter.cs#L143.
|
||||
.Append('\\')
|
||||
.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Get a valid filename for use inside a zip file. Avoids backslashes being incorrectly converted to directories.
|
||||
/// Create a valid filename which should work across all platforms.
|
||||
/// </summary>
|
||||
public static string GetValidArchiveContentFilename(this string filename)
|
||||
{
|
||||
foreach (char c in invalid_filename_characters)
|
||||
filename = filename.Replace(c, '_');
|
||||
return filename;
|
||||
}
|
||||
/// <remarks>
|
||||
/// This function replaces all characters not included in a very pessimistic list which should be compatible
|
||||
/// across all operating systems. We are using this in place of <see cref="Path.GetInvalidFileNameChars"/> as
|
||||
/// that function does not have per-platform considerations (and is only made to work on windows).
|
||||
/// </remarks>
|
||||
public static string GetValidFilename(this string filename) => invalid_filename_chars.Replace(filename, "_");
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using Markdig;
|
||||
using Markdig.Extensions.AutoLinks;
|
||||
using Markdig.Extensions.CustomContainers;
|
||||
using Markdig.Extensions.EmphasisExtras;
|
||||
using Markdig.Extensions.Footnotes;
|
||||
using Markdig.Extensions.Tables;
|
||||
@ -32,6 +33,12 @@ namespace osu.Game.Graphics.Containers.Markdown
|
||||
/// <seealso cref="AutoLinkExtension"/>
|
||||
protected virtual bool Autolinks => false;
|
||||
|
||||
/// <summary>
|
||||
/// Allows this markdown container to parse custom containers (used for flags and infoboxes).
|
||||
/// </summary>
|
||||
/// <seealso cref="CustomContainerExtension"/>
|
||||
protected virtual bool CustomContainers => false;
|
||||
|
||||
public OsuMarkdownContainer()
|
||||
{
|
||||
LineSpacing = 21;
|
||||
@ -107,6 +114,9 @@ namespace osu.Game.Graphics.Containers.Markdown
|
||||
if (Autolinks)
|
||||
pipeline = pipeline.UseAutoLinks();
|
||||
|
||||
if (CustomContainers)
|
||||
pipeline.UseCustomContainers();
|
||||
|
||||
return pipeline.Build();
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,9 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Markdig.Extensions.CustomContainers;
|
||||
using Markdig.Syntax.Inlines;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
@ -11,6 +14,9 @@ using osu.Framework.Graphics.Containers.Markdown;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.Containers.Markdown
|
||||
{
|
||||
@ -33,6 +39,31 @@ namespace osu.Game.Graphics.Containers.Markdown
|
||||
protected override SpriteText CreateEmphasisedSpriteText(bool bold, bool italic)
|
||||
=> CreateSpriteText().With(t => t.Font = t.Font.With(weight: bold ? FontWeight.Bold : FontWeight.Regular, italics: italic));
|
||||
|
||||
protected override void AddCustomComponent(CustomContainerInline inline)
|
||||
{
|
||||
if (!(inline.FirstChild is LiteralInline literal))
|
||||
{
|
||||
base.AddCustomComponent(inline);
|
||||
return;
|
||||
}
|
||||
|
||||
string[] attributes = literal.Content.ToString().Trim(' ', '{', '}').Split();
|
||||
string flagAttribute = attributes.SingleOrDefault(a => a.StartsWith(@"flag", StringComparison.Ordinal));
|
||||
|
||||
if (flagAttribute == null)
|
||||
{
|
||||
base.AddCustomComponent(inline);
|
||||
return;
|
||||
}
|
||||
|
||||
string flag = flagAttribute.Split('=').Last().Trim('"');
|
||||
|
||||
if (!Enum.TryParse<CountryCode>(flag, out var countryCode))
|
||||
countryCode = CountryCode.Unknown;
|
||||
|
||||
AddDrawable(new DrawableFlag(countryCode) { Size = new Vector2(20, 15) });
|
||||
}
|
||||
|
||||
private class OsuMarkdownInlineCode : Container
|
||||
{
|
||||
[Resolved]
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ using osu.Game.Configuration;
|
||||
namespace osu.Game.Graphics.Cursor
|
||||
{
|
||||
/// <summary>
|
||||
/// A container which provides the main <see cref="Cursor.MenuCursor"/>.
|
||||
/// A container which provides the main <see cref="MenuCursorContainer"/>.
|
||||
/// Also handles cases where a more localised cursor is provided by another component (via <see cref="IProvideCursor"/>).
|
||||
/// </summary>
|
||||
public class GlobalCursorDisplay : Container, IProvideCursor
|
||||
@ -23,7 +23,9 @@ namespace osu.Game.Graphics.Cursor
|
||||
/// </summary>
|
||||
internal bool ShowCursor = true;
|
||||
|
||||
public CursorContainer MenuCursor { get; }
|
||||
CursorContainer IProvideCursor.Cursor => MenuCursor;
|
||||
|
||||
public MenuCursorContainer MenuCursor { get; }
|
||||
|
||||
public bool ProvidingUserCursor => true;
|
||||
|
||||
@ -42,8 +44,8 @@ namespace osu.Game.Graphics.Cursor
|
||||
{
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
MenuCursor = new MenuCursor { State = { Value = Visibility.Hidden } },
|
||||
Content = new Container { RelativeSizeAxes = Axes.Both }
|
||||
Content = new Container { RelativeSizeAxes = Axes.Both },
|
||||
MenuCursor = new MenuCursorContainer { State = { Value = Visibility.Hidden } }
|
||||
});
|
||||
}
|
||||
|
||||
@ -64,7 +66,7 @@ namespace osu.Game.Graphics.Cursor
|
||||
|
||||
if (!hasValidInput || !ShowCursor)
|
||||
{
|
||||
currentOverrideProvider?.MenuCursor?.Hide();
|
||||
currentOverrideProvider?.Cursor?.Hide();
|
||||
currentOverrideProvider = null;
|
||||
return;
|
||||
}
|
||||
@ -83,8 +85,8 @@ namespace osu.Game.Graphics.Cursor
|
||||
if (currentOverrideProvider == newOverrideProvider)
|
||||
return;
|
||||
|
||||
currentOverrideProvider?.MenuCursor?.Hide();
|
||||
newOverrideProvider.MenuCursor?.Show();
|
||||
currentOverrideProvider?.Cursor?.Hide();
|
||||
newOverrideProvider.Cursor?.Show();
|
||||
|
||||
currentOverrideProvider = newOverrideProvider;
|
||||
}
|
||||
|
@ -17,10 +17,10 @@ namespace osu.Game.Graphics.Cursor
|
||||
/// The cursor provided by this <see cref="IDrawable"/>.
|
||||
/// May be null if no cursor should be visible.
|
||||
/// </summary>
|
||||
CursorContainer MenuCursor { get; }
|
||||
CursorContainer Cursor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="MenuCursor"/> should be displayed as the singular user cursor. This will temporarily hide any other user cursor.
|
||||
/// Whether <see cref="Cursor"/> should be displayed as the singular user cursor. This will temporarily hide any other user cursor.
|
||||
/// This value is checked every frame and may be used to control whether multiple cursors are displayed (e.g. watching replays).
|
||||
/// </summary>
|
||||
bool ProvidingUserCursor { get; }
|
||||
|
@ -1,10 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
@ -21,24 +18,43 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.Cursor
|
||||
{
|
||||
public class MenuCursor : CursorContainer
|
||||
public class MenuCursorContainer : CursorContainer
|
||||
{
|
||||
private readonly IBindable<bool> screenshotCursorVisibility = new Bindable<bool>(true);
|
||||
public override bool IsPresent => screenshotCursorVisibility.Value && base.IsPresent;
|
||||
|
||||
private bool hideCursorOnNonMouseInput;
|
||||
|
||||
public bool HideCursorOnNonMouseInput
|
||||
{
|
||||
get => hideCursorOnNonMouseInput;
|
||||
set
|
||||
{
|
||||
if (hideCursorOnNonMouseInput == value)
|
||||
return;
|
||||
|
||||
hideCursorOnNonMouseInput = value;
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
|
||||
protected override Drawable CreateCursor() => activeCursor = new Cursor();
|
||||
|
||||
private Cursor activeCursor;
|
||||
private Cursor activeCursor = null!;
|
||||
|
||||
private Bindable<bool> cursorRotate;
|
||||
private DragRotationState dragRotationState;
|
||||
private Vector2 positionMouseDown;
|
||||
|
||||
private Sample tapSample;
|
||||
private Vector2 lastMovePosition;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load([NotNull] OsuConfigManager config, [CanBeNull] ScreenshotManager screenshotManager, AudioManager audio)
|
||||
private Bindable<bool> cursorRotate = null!;
|
||||
private Sample tapSample = null!;
|
||||
|
||||
private MouseInputDetector mouseInputDetector = null!;
|
||||
|
||||
private bool visible;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config, ScreenshotManager? screenshotManager, AudioManager audio)
|
||||
{
|
||||
cursorRotate = config.GetBindable<bool>(OsuSetting.CursorRotation);
|
||||
|
||||
@ -46,6 +62,45 @@ namespace osu.Game.Graphics.Cursor
|
||||
screenshotCursorVisibility.BindTo(screenshotManager.CursorVisibility);
|
||||
|
||||
tapSample = audio.Samples.Get(@"UI/cursor-tap");
|
||||
|
||||
Add(mouseInputDetector = new MouseInputDetector());
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private OsuGame? game { get; set; }
|
||||
|
||||
private readonly IBindable<bool> lastInputWasMouse = new BindableBool();
|
||||
private readonly IBindable<bool> isIdle = new BindableBool();
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
lastInputWasMouse.BindTo(mouseInputDetector.LastInputWasMouseSource);
|
||||
lastInputWasMouse.BindValueChanged(_ => updateState(), true);
|
||||
|
||||
if (game != null)
|
||||
{
|
||||
isIdle.BindTo(game.IsIdle);
|
||||
isIdle.BindValueChanged(_ => updateState());
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateState(ValueChangedEvent<Visibility> state) => updateState();
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
bool combinedVisibility = State.Value == Visibility.Visible && (lastInputWasMouse.Value || !hideCursorOnNonMouseInput) && !isIdle.Value;
|
||||
|
||||
if (visible == combinedVisibility)
|
||||
return;
|
||||
|
||||
visible = combinedVisibility;
|
||||
|
||||
if (visible)
|
||||
PopIn();
|
||||
else
|
||||
PopOut();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -163,11 +218,11 @@ namespace osu.Game.Graphics.Cursor
|
||||
|
||||
public class Cursor : Container
|
||||
{
|
||||
private Container cursorContainer;
|
||||
private Bindable<float> cursorScale;
|
||||
private Container cursorContainer = null!;
|
||||
private Bindable<float> cursorScale = null!;
|
||||
private const float base_scale = 0.15f;
|
||||
|
||||
public Sprite AdditiveLayer;
|
||||
public Sprite AdditiveLayer = null!;
|
||||
|
||||
public Cursor()
|
||||
{
|
||||
@ -204,6 +259,40 @@ namespace osu.Game.Graphics.Cursor
|
||||
}
|
||||
}
|
||||
|
||||
private class MouseInputDetector : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the last input applied to the game is sourced from mouse.
|
||||
/// </summary>
|
||||
public IBindable<bool> LastInputWasMouseSource => lastInputWasMouseSource;
|
||||
|
||||
private readonly Bindable<bool> lastInputWasMouseSource = new Bindable<bool>();
|
||||
|
||||
public MouseInputDetector()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
protected override bool Handle(UIEvent e)
|
||||
{
|
||||
switch (e)
|
||||
{
|
||||
case MouseDownEvent:
|
||||
case MouseMoveEvent:
|
||||
lastInputWasMouseSource.Value = true;
|
||||
return false;
|
||||
|
||||
case KeyDownEvent keyDown when !keyDown.Repeat:
|
||||
case JoystickPressEvent:
|
||||
case MidiDownEvent:
|
||||
lastInputWasMouseSource.Value = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private enum DragRotationState
|
||||
{
|
||||
NotDragging,
|
@ -10,7 +10,6 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.OSD;
|
||||
@ -94,15 +93,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
private void copyUrl()
|
||||
{
|
||||
host.GetClipboard()?.SetText(Link);
|
||||
onScreenDisplay?.Display(new CopyUrlToast(ToastStrings.UrlCopied));
|
||||
}
|
||||
|
||||
private class CopyUrlToast : Toast
|
||||
{
|
||||
public CopyUrlToast(LocalisableString value)
|
||||
: base(UserInterfaceStrings.GeneralHeader, value, "")
|
||||
{
|
||||
}
|
||||
onScreenDisplay?.Display(new CopyUrlToast());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
[Description("button")]
|
||||
Button,
|
||||
|
||||
[Description("button-sidebar")]
|
||||
ButtonSidebar,
|
||||
|
||||
[Description("toolbar")]
|
||||
Toolbar,
|
||||
|
||||
|
@ -26,24 +26,24 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
set
|
||||
{
|
||||
if (labelText != null)
|
||||
labelText.Text = value;
|
||||
if (LabelTextFlowContainer != null)
|
||||
LabelTextFlowContainer.Text = value;
|
||||
}
|
||||
}
|
||||
|
||||
public MarginPadding LabelPadding
|
||||
{
|
||||
get => labelText?.Padding ?? new MarginPadding();
|
||||
get => LabelTextFlowContainer?.Padding ?? new MarginPadding();
|
||||
set
|
||||
{
|
||||
if (labelText != null)
|
||||
labelText.Padding = value;
|
||||
if (LabelTextFlowContainer != null)
|
||||
LabelTextFlowContainer.Padding = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly Nub Nub;
|
||||
|
||||
private readonly OsuTextFlowContainer labelText;
|
||||
protected readonly OsuTextFlowContainer LabelTextFlowContainer;
|
||||
private Sample sampleChecked;
|
||||
private Sample sampleUnchecked;
|
||||
|
||||
@ -56,7 +56,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
labelText = new OsuTextFlowContainer(ApplyLabelParameters)
|
||||
LabelTextFlowContainer = new OsuTextFlowContainer(ApplyLabelParameters)
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
@ -70,19 +70,19 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Nub.Anchor = Anchor.CentreRight;
|
||||
Nub.Origin = Anchor.CentreRight;
|
||||
Nub.Margin = new MarginPadding { Right = nub_padding };
|
||||
labelText.Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding * 2 };
|
||||
LabelTextFlowContainer.Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding * 2 };
|
||||
}
|
||||
else
|
||||
{
|
||||
Nub.Anchor = Anchor.CentreLeft;
|
||||
Nub.Origin = Anchor.CentreLeft;
|
||||
Nub.Margin = new MarginPadding { Left = nub_padding };
|
||||
labelText.Padding = new MarginPadding { Left = Nub.EXPANDED_SIZE + nub_padding * 2 };
|
||||
LabelTextFlowContainer.Padding = new MarginPadding { Left = Nub.EXPANDED_SIZE + nub_padding * 2 };
|
||||
}
|
||||
|
||||
Nub.Current.BindTo(Current);
|
||||
|
||||
Current.DisabledChanged += disabled => labelText.Alpha = Nub.Alpha = disabled ? 0.3f : 1;
|
||||
Current.DisabledChanged += disabled => LabelTextFlowContainer.Alpha = Nub.Alpha = disabled ? 0.3f : 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -44,6 +44,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
public virtual LocalisableString TooltipText { get; private set; }
|
||||
|
||||
public bool PlaySamplesOnAdjust { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to format the tooltip as a percentage or the actual value.
|
||||
/// </summary>
|
||||
@ -187,6 +189,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
private void playSample(T value)
|
||||
{
|
||||
if (!PlaySamplesOnAdjust)
|
||||
return;
|
||||
|
||||
if (Clock == null || Clock.CurrentTime - lastSampleTime <= 30)
|
||||
return;
|
||||
|
||||
|
@ -31,6 +31,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay();
|
||||
|
||||
protected override Drawable CreateHiddenToggleButton() => new OsuDirectorySelectorHiddenToggle { Current = { BindTarget = ShowHiddenItems } };
|
||||
|
||||
protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory);
|
||||
|
||||
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName);
|
||||
|
@ -0,0 +1,38 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
internal class OsuDirectorySelectorHiddenToggle : OsuCheckbox
|
||||
{
|
||||
public OsuDirectorySelectorHiddenToggle()
|
||||
{
|
||||
RelativeSizeAxes = Axes.None;
|
||||
AutoSizeAxes = Axes.None;
|
||||
Size = new Vector2(100, 50);
|
||||
Anchor = Anchor.CentreLeft;
|
||||
Origin = Anchor.CentreLeft;
|
||||
LabelTextFlowContainer.Anchor = Anchor.CentreLeft;
|
||||
LabelTextFlowContainer.Origin = Anchor.CentreLeft;
|
||||
LabelText = @"Show hidden";
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OverlayColourProvider? overlayColourProvider, OsuColour colours)
|
||||
{
|
||||
if (overlayColourProvider != null)
|
||||
return;
|
||||
|
||||
Nub.AccentColour = colours.GreySeaFoamLighter;
|
||||
Nub.GlowingAccentColour = Color4.White;
|
||||
Nub.GlowColour = Color4.White;
|
||||
}
|
||||
}
|
||||
}
|
@ -33,6 +33,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay();
|
||||
|
||||
protected override Drawable CreateHiddenToggleButton() => new OsuDirectorySelectorHiddenToggle { Current = { BindTarget = ShowHiddenItems } };
|
||||
|
||||
protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory);
|
||||
|
||||
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName);
|
||||
|
@ -55,13 +55,13 @@ namespace osu.Game.Input.Bindings
|
||||
{
|
||||
// The first fire of this is a bit redundant as this is being called in base.LoadComplete,
|
||||
// but this is safest in case the subscription is restored after a context recycle.
|
||||
reloadMappings(sender.AsQueryable());
|
||||
ReloadMappings(sender.AsQueryable());
|
||||
});
|
||||
|
||||
base.LoadComplete();
|
||||
}
|
||||
|
||||
protected override void ReloadMappings() => reloadMappings(queryRealmKeyBindings(realm.Realm));
|
||||
protected sealed override void ReloadMappings() => ReloadMappings(queryRealmKeyBindings(realm.Realm));
|
||||
|
||||
private IQueryable<RealmKeyBinding> queryRealmKeyBindings(Realm realm)
|
||||
{
|
||||
@ -70,7 +70,7 @@ namespace osu.Game.Input.Bindings
|
||||
.Where(b => b.RulesetName == rulesetName && b.Variant == variant);
|
||||
}
|
||||
|
||||
private void reloadMappings(IQueryable<RealmKeyBinding> realmKeyBindings)
|
||||
protected virtual void ReloadMappings(IQueryable<RealmKeyBinding> realmKeyBindings)
|
||||
{
|
||||
var defaults = DefaultKeyBindings.ToList();
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +89,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString Collections => new TranslatableString(getKey(@"collections"), @"Collections");
|
||||
|
||||
/// <summary>
|
||||
/// "Mod presets"
|
||||
/// </summary>
|
||||
public static LocalisableString ModPresets => new TranslatableString(getKey(@"mod_presets"), @"Mod presets");
|
||||
|
||||
/// <summary>
|
||||
/// "Name"
|
||||
/// </summary>
|
||||
|
@ -44,11 +44,6 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ClearAllCaches => new TranslatableString(getKey(@"clear_all_caches"), @"Clear all caches");
|
||||
|
||||
/// <summary>
|
||||
/// "Compact realm"
|
||||
/// </summary>
|
||||
public static LocalisableString CompactRealm => new TranslatableString(getKey(@"compact_realm"), @"Compact realm");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString RunSetupWizard => new TranslatableString(getKey(@"run_setup_wizard"), @"Run setup wizard");
|
||||
|
||||
/// <summary>
|
||||
/// "You are running the latest release ({0})"
|
||||
/// </summary>
|
||||
public static LocalisableString RunningLatestRelease(string version) => new TranslatableString(getKey(@"running_latest_release"), @"You are running the latest release ({0})", version);
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -19,6 +19,41 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString SelectDirectory => new TranslatableString(getKey(@"select_directory"), @"Select directory");
|
||||
|
||||
/// <summary>
|
||||
/// "Migration in progress"
|
||||
/// </summary>
|
||||
public static LocalisableString MigrationInProgress => new TranslatableString(getKey(@"migration_in_progress"), @"Migration in progress");
|
||||
|
||||
/// <summary>
|
||||
/// "This could take a few minutes depending on the speed of your disk(s)."
|
||||
/// </summary>
|
||||
public static LocalisableString MigrationDescription => new TranslatableString(getKey(@"migration_description"), @"This could take a few minutes depending on the speed of your disk(s).");
|
||||
|
||||
/// <summary>
|
||||
/// "Please avoid interacting with the game!"
|
||||
/// </summary>
|
||||
public static LocalisableString ProhibitedInteractDuringMigration => new TranslatableString(getKey(@"prohibited_interact_during_migration"), @"Please avoid interacting with the game!");
|
||||
|
||||
/// <summary>
|
||||
/// "Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up."
|
||||
/// </summary>
|
||||
public static LocalisableString FailedCleanupNotification => new TranslatableString(getKey(@"failed_cleanup_notification"), @"Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up.");
|
||||
|
||||
/// <summary>
|
||||
/// "Please select a new location"
|
||||
/// </summary>
|
||||
public static LocalisableString SelectNewLocation => new TranslatableString(getKey(@"select_new_location"), @"Please select a new location");
|
||||
|
||||
/// <summary>
|
||||
/// "The target directory already seems to have an osu! install. Use that data instead?"
|
||||
/// </summary>
|
||||
public static LocalisableString TargetDirectoryAlreadyInstalledOsu => new TranslatableString(getKey(@"target_directory_already_installed_osu"), @"The target directory already seems to have an osu! install. Use that data instead?");
|
||||
|
||||
/// <summary>
|
||||
/// "To complete this operation, osu! will close. Please open it again to use the new data location."
|
||||
/// </summary>
|
||||
public static LocalisableString RestartAndReOpenRequiredForCompletion => new TranslatableString(getKey(@"restart_and_re_open_required_for_completion"), @"To complete this operation, osu! will close. Please open it again to use the new data location.");
|
||||
|
||||
/// <summary>
|
||||
/// "Import beatmaps from stable"
|
||||
/// </summary>
|
||||
@ -84,6 +119,26 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString RestoreAllRecentlyDeletedModPresets => new TranslatableString(getKey(@"restore_all_recently_deleted_mod_presets"), @"Restore all recently deleted mod presets");
|
||||
|
||||
/// <summary>
|
||||
/// "Deleted all collections!"
|
||||
/// </summary>
|
||||
public static LocalisableString DeletedAllCollections => new TranslatableString(getKey(@"deleted_all_collections"), @"Deleted all collections!");
|
||||
|
||||
/// <summary>
|
||||
/// "Deleted all mod presets!"
|
||||
/// </summary>
|
||||
public static LocalisableString DeletedAllModPresets => new TranslatableString(getKey(@"deleted_all_mod_presets"), @"Deleted all mod presets!");
|
||||
|
||||
/// <summary>
|
||||
/// "Restored all deleted mod presets!"
|
||||
/// </summary>
|
||||
public static LocalisableString RestoredAllDeletedModPresets => new TranslatableString(getKey(@"restored_all_deleted_mod_presets"), @"Restored all deleted mod presets!");
|
||||
|
||||
/// <summary>
|
||||
/// "Please select your osu!stable install location"
|
||||
/// </summary>
|
||||
public static LocalisableString StableDirectorySelectHeader => new TranslatableString(getKey(@"stable_directory_select_header"), @"Please select your osu!stable install location");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
24
osu.Game/Localisation/PopupDialogStrings.cs
Normal file
24
osu.Game/Localisation/PopupDialogStrings.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class PopupDialogStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.PopupDialog";
|
||||
|
||||
/// <summary>
|
||||
/// "Are you sure you want to update this beatmap?"
|
||||
/// </summary>
|
||||
public static LocalisableString UpdateLocallyModifiedText => new TranslatableString(getKey(@"update_locally_modified_text"), @"Are you sure you want to update this beatmap?");
|
||||
|
||||
/// <summary>
|
||||
/// "This will discard all local changes you have on that beatmap."
|
||||
/// </summary>
|
||||
public static LocalisableString UpdateLocallyModifiedDescription => new TranslatableString(getKey(@"update_locally_modified_description"), @"This will discard all local changes you have on that beatmap.");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
@ -19,6 +19,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString NoTabletDetected => new TranslatableString(getKey(@"no_tablet_detected"), @"No tablet detected!");
|
||||
|
||||
/// <summary>
|
||||
/// "If your tablet is not detected, please read [this FAQ]({0}) for troubleshooting steps."
|
||||
/// </summary>
|
||||
public static LocalisableString NoTabletDetectedDescription(string url) => new TranslatableString(getKey(@"no_tablet_detected_description"), @"If your tablet is not detected, please read [this FAQ]({0}) for troubleshooting steps.", url);
|
||||
|
||||
/// <summary>
|
||||
/// "Reset to full area"
|
||||
/// </summary>
|
||||
|
28
osu.Game/Online/API/Requests/CommentDeleteRequest.cs
Normal file
28
osu.Game/Online/API/Requests/CommentDeleteRequest.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.Net.Http;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class CommentDeleteRequest : APIRequest<CommentBundle>
|
||||
{
|
||||
public readonly long CommentId;
|
||||
|
||||
public CommentDeleteRequest(long id)
|
||||
{
|
||||
CommentId = id;
|
||||
}
|
||||
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var req = base.CreateWebRequest();
|
||||
req.Method = HttpMethod.Delete;
|
||||
return req;
|
||||
}
|
||||
|
||||
protected override string Target => $@"comments/{CommentId}";
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
|
||||
@ -16,18 +14,18 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
[JsonProperty(@"parent_id")]
|
||||
public long? ParentId { get; set; }
|
||||
|
||||
public Comment ParentComment { get; set; }
|
||||
public Comment? ParentComment { get; set; }
|
||||
|
||||
[JsonProperty(@"user_id")]
|
||||
public long? UserId { get; set; }
|
||||
|
||||
public APIUser User { get; set; }
|
||||
public APIUser? User { get; set; }
|
||||
|
||||
[JsonProperty(@"message")]
|
||||
public string Message { get; set; }
|
||||
public string Message { get; set; } = null!;
|
||||
|
||||
[JsonProperty(@"message_html")]
|
||||
public string MessageHtml { get; set; }
|
||||
public string? MessageHtml { get; set; }
|
||||
|
||||
[JsonProperty(@"replies_count")]
|
||||
public int RepliesCount { get; set; }
|
||||
@ -36,13 +34,13 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
public int VotesCount { get; set; }
|
||||
|
||||
[JsonProperty(@"commenatble_type")]
|
||||
public string CommentableType { get; set; }
|
||||
public string CommentableType { get; set; } = null!;
|
||||
|
||||
[JsonProperty(@"commentable_id")]
|
||||
public int CommentableId { get; set; }
|
||||
|
||||
[JsonProperty(@"legacy_name")]
|
||||
public string LegacyName { get; set; }
|
||||
public string? LegacyName { get; set; }
|
||||
|
||||
[JsonProperty(@"created_at")]
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
@ -62,7 +60,7 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
[JsonProperty(@"pinned")]
|
||||
public bool Pinned { get; set; }
|
||||
|
||||
public APIUser EditedUser { get; set; }
|
||||
public APIUser? EditedUser { get; set; }
|
||||
|
||||
public bool IsTopLevel => !ParentId.HasValue;
|
||||
|
||||
|
@ -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")]
|
||||
@ -114,6 +115,7 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
[JsonProperty("has_replay")]
|
||||
public bool HasReplay { get; set; }
|
||||
|
||||
// These properties are calculated or not relevant to any external usage.
|
||||
public bool ShouldSerializeID() => false;
|
||||
public bool ShouldSerializeUser() => false;
|
||||
public bool ShouldSerializeBeatmap() => false;
|
||||
@ -122,6 +124,18 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
public bool ShouldSerializeOnlineID() => false;
|
||||
public bool ShouldSerializeHasReplay() => false;
|
||||
|
||||
// These fields only need to be serialised if they hold values.
|
||||
// Generally this is required because this model may be used by server-side components, but
|
||||
// we don't want to bother sending these fields in score submission requests, for instance.
|
||||
public bool ShouldSerializeEndedAt() => EndedAt != default;
|
||||
public bool ShouldSerializeStartedAt() => StartedAt != default;
|
||||
public bool ShouldSerializeLegacyScoreId() => LegacyScoreId != null;
|
||||
public bool ShouldSerializeLegacyTotalScore() => LegacyTotalScore != null;
|
||||
public bool ShouldSerializeMods() => Mods.Length > 0;
|
||||
public bool ShouldSerializeUserID() => UserID > 0;
|
||||
public bool ShouldSerializeBeatmapID() => BeatmapID > 0;
|
||||
public bool ShouldSerializeBuildID() => BuildID != null;
|
||||
|
||||
#endregion
|
||||
|
||||
public override string ToString() => $"score_id: {ID} user_id: {UserID}";
|
||||
@ -140,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;
|
||||
}
|
||||
@ -152,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.
|
||||
|
@ -65,7 +65,7 @@ namespace osu.Game.Online.Rooms
|
||||
[CanBeNull]
|
||||
public MultiplayerScoresAround ScoresAround { get; set; }
|
||||
|
||||
public ScoreInfo CreateScoreInfo(RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap)
|
||||
public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap)
|
||||
{
|
||||
var ruleset = rulesets.GetRuleset(playlistItem.RulesetID);
|
||||
if (ruleset == null)
|
||||
@ -90,6 +90,8 @@ namespace osu.Game.Online.Rooms
|
||||
Position = Position,
|
||||
};
|
||||
|
||||
scoreManager.PopulateMaximumStatistics(scoreInfo);
|
||||
|
||||
return scoreInfo;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +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.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -58,7 +58,7 @@ namespace osu.Game.Online.Spectator
|
||||
{
|
||||
await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), state);
|
||||
}
|
||||
catch (HubException exception)
|
||||
catch (Exception exception)
|
||||
{
|
||||
if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE)
|
||||
{
|
||||
|
@ -70,6 +70,7 @@ namespace osu.Game
|
||||
/// The full osu! experience. Builds on top of <see cref="OsuGameBase"/> to add menus and binding logic
|
||||
/// for initial components that are generally retrieved via DI.
|
||||
/// </summary>
|
||||
[Cached(typeof(OsuGame))]
|
||||
public class OsuGame : OsuGameBase, IKeyBindingHandler<GlobalAction>, ILocalUserPlayInfo, IPerformFromScreenRunner, IOverlayManager, ILinkHandler
|
||||
{
|
||||
/// <summary>
|
||||
@ -136,6 +137,11 @@ namespace osu.Game
|
||||
|
||||
private IdleTracker idleTracker;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user is currently in an idle state.
|
||||
/// </summary>
|
||||
public IBindable<bool> IsIdle => idleTracker.IsIdle;
|
||||
|
||||
/// <summary>
|
||||
/// Whether overlays should be able to be opened game-wide. Value is sourced from the current active screen.
|
||||
/// </summary>
|
||||
@ -173,6 +179,8 @@ namespace osu.Game
|
||||
|
||||
private Bindable<string> configRuleset;
|
||||
|
||||
private Bindable<bool> applySafeAreaConsiderations;
|
||||
|
||||
private Bindable<float> uiScale;
|
||||
|
||||
private Bindable<string> configSkin;
|
||||
@ -266,8 +274,6 @@ namespace osu.Game
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
dependencies.CacheAs(this);
|
||||
|
||||
SentryLogger.AttachUser(API.LocalUser);
|
||||
|
||||
dependencies.Cache(osuLogo = new OsuLogo { Alpha = 0 });
|
||||
@ -276,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
|
||||
{
|
||||
@ -308,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;
|
||||
@ -563,6 +569,15 @@ namespace osu.Game
|
||||
|
||||
// This should be able to be performed from song select, but that is disabled for now
|
||||
// due to the weird decoupled ruleset logic (which can cause a crash in certain filter scenarios).
|
||||
//
|
||||
// As a special case, if the beatmap and ruleset already match, allow immediately displaying the score from song select.
|
||||
// This is guaranteed to not crash, and feels better from a user's perspective (ie. if they are clicking a score in the
|
||||
// song select leaderboard).
|
||||
IEnumerable<Type> validScreens =
|
||||
Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap) && Ruleset.Value.Equals(databasedScore.ScoreInfo.Ruleset)
|
||||
? new[] { typeof(SongSelect) }
|
||||
: Array.Empty<Type>();
|
||||
|
||||
PerformFromScreen(screen =>
|
||||
{
|
||||
Logger.Log($"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset}) to match score");
|
||||
@ -580,7 +595,7 @@ namespace osu.Game
|
||||
screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo, false));
|
||||
break;
|
||||
}
|
||||
});
|
||||
}, validScreens: validScreens);
|
||||
}
|
||||
|
||||
public override Task Import(params ImportTask[] imports)
|
||||
@ -1320,6 +1335,8 @@ namespace osu.Game
|
||||
OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode);
|
||||
API.Activity.BindTo(newOsuScreen.Activity);
|
||||
|
||||
GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newOsuScreen.HideMenuCursorOnNonMouseInput;
|
||||
|
||||
if (newOsuScreen.HideOverlaysOnEnter)
|
||||
CloseAllOverlays();
|
||||
else
|
||||
|
@ -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;
|
||||
@ -62,6 +67,7 @@ namespace osu.Game
|
||||
/// Unlike <see cref="OsuGame"/>, this class will not load any kind of UI, allowing it to be used
|
||||
/// for provide dependencies to test cases without interfering with them.
|
||||
/// </summary>
|
||||
[Cached(typeof(OsuGameBase))]
|
||||
public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider
|
||||
{
|
||||
public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" };
|
||||
@ -188,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.
|
||||
@ -253,7 +261,6 @@ namespace osu.Game
|
||||
largeStore.AddTextureSource(Host.CreateTextureLoaderStore(new OnlineStore()));
|
||||
dependencies.Cache(largeStore);
|
||||
|
||||
dependencies.CacheAs(this);
|
||||
dependencies.CacheAs(LocalConfig);
|
||||
|
||||
InitialiseFonts();
|
||||
@ -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();
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics;
|
||||
@ -22,7 +20,13 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using System.Collections.Specialized;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Overlays.Comments.Buttons;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Overlays.OSD;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.Comments
|
||||
@ -31,7 +35,7 @@ namespace osu.Game.Overlays.Comments
|
||||
{
|
||||
private const int avatar_size = 40;
|
||||
|
||||
public Action<DrawableComment, int> RepliesRequested;
|
||||
public Action<DrawableComment, int> RepliesRequested = null!;
|
||||
|
||||
public readonly Comment Comment;
|
||||
|
||||
@ -45,13 +49,35 @@ namespace osu.Game.Overlays.Comments
|
||||
|
||||
private int currentPage;
|
||||
|
||||
private FillFlowContainer childCommentsVisibilityContainer;
|
||||
private FillFlowContainer childCommentsContainer;
|
||||
private LoadRepliesButton loadRepliesButton;
|
||||
private ShowMoreRepliesButton showMoreButton;
|
||||
private ShowRepliesButton showRepliesButton;
|
||||
private ChevronButton chevronButton;
|
||||
private DeletedCommentsCounter deletedCommentsCounter;
|
||||
/// <summary>
|
||||
/// Local field for tracking comment state. Initialized from Comment.IsDeleted, may change when deleting was requested by user.
|
||||
/// </summary>
|
||||
public bool WasDeleted { get; protected set; }
|
||||
|
||||
private FillFlowContainer childCommentsVisibilityContainer = null!;
|
||||
private FillFlowContainer childCommentsContainer = null!;
|
||||
private LoadRepliesButton loadRepliesButton = null!;
|
||||
private ShowMoreRepliesButton showMoreButton = null!;
|
||||
private ShowRepliesButton showRepliesButton = null!;
|
||||
private ChevronButton chevronButton = null!;
|
||||
private LinkFlowContainer actionsContainer = null!;
|
||||
private LoadingSpinner actionsLoading = null!;
|
||||
private DeletedCommentsCounter deletedCommentsCounter = null!;
|
||||
private OsuSpriteText deletedLabel = null!;
|
||||
private GridContainer content = null!;
|
||||
private VotePill votePill = null!;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private IDialogOverlay? dialogOverlay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; } = null!;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private OnScreenDisplay? onScreenDisplay { get; set; }
|
||||
|
||||
public DrawableComment(Comment comment)
|
||||
{
|
||||
@ -64,8 +90,6 @@ namespace osu.Game.Overlays.Comments
|
||||
LinkFlowContainer username;
|
||||
FillFlowContainer info;
|
||||
CommentMarkdownContainer message;
|
||||
GridContainer content;
|
||||
VotePill votePill;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
@ -148,9 +172,9 @@ namespace osu.Game.Overlays.Comments
|
||||
},
|
||||
Comment.Pinned ? new PinnedCommentNotice() : Empty(),
|
||||
new ParentUsername(Comment),
|
||||
new OsuSpriteText
|
||||
deletedLabel = new OsuSpriteText
|
||||
{
|
||||
Alpha = Comment.IsDeleted ? 1 : 0,
|
||||
Alpha = 0f,
|
||||
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold),
|
||||
Text = CommentsStrings.Deleted
|
||||
}
|
||||
@ -163,16 +187,36 @@ namespace osu.Game.Overlays.Comments
|
||||
DocumentMargin = new MarginPadding(0),
|
||||
DocumentPadding = new MarginPadding(0),
|
||||
},
|
||||
info = new FillFlowContainer
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(10, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new DrawableDate(Comment.CreatedAt, 12, false)
|
||||
info = new FillFlowContainer
|
||||
{
|
||||
Colour = colourProvider.Foreground1
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(10, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new DrawableDate(Comment.CreatedAt, 12, false)
|
||||
{
|
||||
Colour = colourProvider.Foreground1
|
||||
}
|
||||
}
|
||||
},
|
||||
actionsContainer = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold))
|
||||
{
|
||||
Name = @"Actions buttons",
|
||||
AutoSizeAxes = Axes.Both,
|
||||
},
|
||||
actionsLoading = new LoadingSpinner
|
||||
{
|
||||
Size = new Vector2(12f),
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -246,9 +290,9 @@ namespace osu.Game.Overlays.Comments
|
||||
if (Comment.UserId.HasValue)
|
||||
username.AddUserLink(Comment.User);
|
||||
else
|
||||
username.AddText(Comment.LegacyName);
|
||||
username.AddText(Comment.LegacyName!);
|
||||
|
||||
if (Comment.EditedAt.HasValue)
|
||||
if (Comment.EditedAt.HasValue && Comment.EditedUser != null)
|
||||
{
|
||||
var font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular);
|
||||
var colour = colourProvider.Foreground1;
|
||||
@ -282,10 +326,16 @@ namespace osu.Game.Overlays.Comments
|
||||
if (Comment.HasMessage)
|
||||
message.Text = Comment.Message;
|
||||
|
||||
if (Comment.IsDeleted)
|
||||
WasDeleted = Comment.IsDeleted;
|
||||
if (WasDeleted)
|
||||
makeDeleted();
|
||||
|
||||
actionsContainer.AddLink("Copy link", copyUrl);
|
||||
actionsContainer.AddArbitraryDrawable(new Container { Width = 10 });
|
||||
|
||||
if (Comment.UserId.HasValue && Comment.UserId.Value == api.LocalUser.Value.Id)
|
||||
{
|
||||
content.FadeColour(OsuColour.Gray(0.5f));
|
||||
votePill.Hide();
|
||||
actionsContainer.AddLink("Delete", deleteComment);
|
||||
}
|
||||
|
||||
if (Comment.IsTopLevel)
|
||||
@ -317,11 +367,63 @@ namespace osu.Game.Overlays.Comments
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms some comment's components to show it as deleted. Invoked both from loading and deleting.
|
||||
/// </summary>
|
||||
private void makeDeleted()
|
||||
{
|
||||
deletedLabel.Show();
|
||||
content.FadeColour(OsuColour.Gray(0.5f));
|
||||
votePill.Hide();
|
||||
actionsContainer.Expire();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes comment deletion with confirmation.
|
||||
/// </summary>
|
||||
private void deleteComment()
|
||||
{
|
||||
if (dialogOverlay == null)
|
||||
deleteCommentRequest();
|
||||
else
|
||||
dialogOverlay.Push(new ConfirmDialog("Do you really want to delete your comment?", deleteCommentRequest));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes comment deletion directly.
|
||||
/// </summary>
|
||||
private void deleteCommentRequest()
|
||||
{
|
||||
actionsContainer.Hide();
|
||||
actionsLoading.Show();
|
||||
var request = new CommentDeleteRequest(Comment.Id);
|
||||
request.Success += _ => Schedule(() =>
|
||||
{
|
||||
actionsLoading.Hide();
|
||||
makeDeleted();
|
||||
WasDeleted = true;
|
||||
if (!ShowDeleted.Value)
|
||||
Hide();
|
||||
});
|
||||
request.Failure += _ => Schedule(() =>
|
||||
{
|
||||
actionsLoading.Hide();
|
||||
actionsContainer.Show();
|
||||
});
|
||||
api.Queue(request);
|
||||
}
|
||||
|
||||
private void copyUrl()
|
||||
{
|
||||
host.GetClipboard()?.SetText($@"{api.APIEndpointUrl}/comments/{Comment.Id}");
|
||||
onScreenDisplay?.Display(new CopyUrlToast());
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
ShowDeleted.BindValueChanged(show =>
|
||||
{
|
||||
if (Comment.IsDeleted)
|
||||
if (WasDeleted)
|
||||
this.FadeTo(show.NewValue ? 1 : 0);
|
||||
}, true);
|
||||
childrenExpanded.BindValueChanged(expanded => childCommentsVisibilityContainer.FadeTo(expanded.NewValue ? 1 : 0), true);
|
||||
@ -425,7 +527,7 @@ namespace osu.Game.Overlays.Comments
|
||||
{
|
||||
public LocalisableString TooltipText => getParentMessage();
|
||||
|
||||
private readonly Comment parentComment;
|
||||
private readonly Comment? parentComment;
|
||||
|
||||
public ParentUsername(Comment comment)
|
||||
{
|
||||
@ -445,7 +547,7 @@ namespace osu.Game.Overlays.Comments
|
||||
new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true),
|
||||
Text = parentComment?.User?.Username ?? parentComment?.LegacyName
|
||||
Text = parentComment?.User?.Username ?? parentComment?.LegacyName!
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.Dialog
|
||||
@ -20,7 +21,7 @@ namespace osu.Game.Overlays.Dialog
|
||||
/// <param name="message">The description of the action to be displayed to the user.</param>
|
||||
/// <param name="onConfirm">An action to perform on confirmation.</param>
|
||||
/// <param name="onCancel">An optional action to perform on cancel.</param>
|
||||
public ConfirmDialog(string message, Action onConfirm, Action onCancel = null)
|
||||
public ConfirmDialog(LocalisableString message, Action onConfirm, Action onCancel = null)
|
||||
{
|
||||
HeaderText = message;
|
||||
BodyText = "Last chance to turn back";
|
||||
|
15
osu.Game/Overlays/OSD/CopyUrlToast.cs
Normal file
15
osu.Game/Overlays/OSD/CopyUrlToast.cs
Normal file
@ -0,0 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Overlays.OSD
|
||||
{
|
||||
public class CopyUrlToast : Toast
|
||||
{
|
||||
public CopyUrlToast()
|
||||
: base(UserInterfaceStrings.GeneralHeader, ToastStrings.UrlCopied, "")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
@ -21,7 +22,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SettingsSlider<double>
|
||||
new VolumeAdjustSlider
|
||||
{
|
||||
LabelText = AudioSettingsStrings.MasterVolume,
|
||||
Current = audio.Volume,
|
||||
@ -35,14 +36,15 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
KeyboardStep = 0.01f,
|
||||
DisplayAsPercentage = true
|
||||
},
|
||||
new SettingsSlider<double>
|
||||
new VolumeAdjustSlider
|
||||
{
|
||||
LabelText = AudioSettingsStrings.EffectVolume,
|
||||
Current = audio.VolumeSample,
|
||||
KeyboardStep = 0.01f,
|
||||
DisplayAsPercentage = true
|
||||
},
|
||||
new SettingsSlider<double>
|
||||
|
||||
new VolumeAdjustSlider
|
||||
{
|
||||
LabelText = AudioSettingsStrings.MusicVolume,
|
||||
Current = audio.VolumeTrack,
|
||||
@ -51,5 +53,15 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private class VolumeAdjustSlider : SettingsSlider<double>
|
||||
{
|
||||
protected override Drawable CreateControl()
|
||||
{
|
||||
var sliderBar = (OsuSliderBar<double>)base.CreateControl();
|
||||
sliderBar.PlaySamplesOnAdjust = false;
|
||||
return sliderBar;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
|
||||
},
|
||||
new SettingsButton
|
||||
{
|
||||
Text = DebugSettingsStrings.CompactRealm,
|
||||
Text = "Compact realm",
|
||||
Action = () =>
|
||||
{
|
||||
// Blocking operations implicitly causes a Compact().
|
||||
|
@ -44,9 +44,12 @@ namespace osu.Game.Overlays.Settings.Sections.General
|
||||
},
|
||||
};
|
||||
|
||||
if (!LanguageExtensions.TryParseCultureCode(frameworkLocale.Value, out var locale))
|
||||
locale = Language.en;
|
||||
languageSelection.Current.Value = locale;
|
||||
frameworkLocale.BindValueChanged(locale =>
|
||||
{
|
||||
if (!LanguageExtensions.TryParseCultureCode(locale.NewValue, out var language))
|
||||
language = Language.en;
|
||||
languageSelection.Current.Value = language;
|
||||
}, true);
|
||||
|
||||
languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToCultureCode());
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
|
||||
{
|
||||
notifications?.Post(new SimpleNotification
|
||||
{
|
||||
Text = $"You are running the latest release ({game.Version})",
|
||||
Text = GeneralSettingsStrings.RunningLatestRelease(game.Version),
|
||||
Icon = FontAwesome.Solid.CheckCircle,
|
||||
});
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using osu.Framework;
|
||||
@ -21,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
|
||||
@ -29,37 +29,42 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
{
|
||||
protected override LocalisableString Header => GraphicsSettingsStrings.LayoutHeader;
|
||||
|
||||
private FillFlowContainer<SettingsSlider<float>> scalingSettings;
|
||||
private FillFlowContainer<SettingsSlider<float>> scalingSettings = null!;
|
||||
|
||||
private readonly Bindable<Display> currentDisplay = new Bindable<Display>();
|
||||
private readonly IBindableList<WindowMode> windowModes = new BindableList<WindowMode>();
|
||||
|
||||
private Bindable<ScalingMode> scalingMode;
|
||||
private Bindable<Size> sizeFullscreen;
|
||||
private Bindable<ScalingMode> scalingMode = null!;
|
||||
private Bindable<Size> sizeFullscreen = null!;
|
||||
|
||||
private readonly BindableList<Size> resolutions = new BindableList<Size>(new[] { new Size(9999, 9999) });
|
||||
private readonly IBindable<FullscreenCapability> fullscreenCapability = new Bindable<FullscreenCapability>(FullscreenCapability.Capable);
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; }
|
||||
private OsuGameBase game { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
private GameHost host { get; set; } = null!;
|
||||
|
||||
private SettingsDropdown<Size> resolutionDropdown;
|
||||
private SettingsDropdown<Display> displayDropdown;
|
||||
private SettingsDropdown<WindowMode> windowModeDropdown;
|
||||
private IWindow? window;
|
||||
|
||||
private Bindable<float> scalingPositionX;
|
||||
private Bindable<float> scalingPositionY;
|
||||
private Bindable<float> scalingSizeX;
|
||||
private Bindable<float> scalingSizeY;
|
||||
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!;
|
||||
private Bindable<float> scalingSizeX = null!;
|
||||
private Bindable<float> scalingSizeY = null!;
|
||||
|
||||
private const int transition_duration = 400;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(FrameworkConfigManager config, OsuConfigManager osuConfig, GameHost host)
|
||||
{
|
||||
window = host.Window;
|
||||
|
||||
scalingMode = osuConfig.GetBindable<ScalingMode>(OsuSetting.Scaling);
|
||||
sizeFullscreen = config.GetBindable<Size>(FrameworkSetting.SizeFullscreen);
|
||||
scalingSizeX = osuConfig.GetBindable<float>(OsuSetting.ScalingSizeX);
|
||||
@ -67,10 +72,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
scalingPositionX = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionX);
|
||||
scalingPositionY = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionY);
|
||||
|
||||
if (host.Window != null)
|
||||
if (window != null)
|
||||
{
|
||||
currentDisplay.BindTo(host.Window.CurrentDisplayBindable);
|
||||
windowModes.BindTo(host.Window.SupportedWindowModes);
|
||||
currentDisplay.BindTo(window.CurrentDisplayBindable);
|
||||
windowModes.BindTo(window.SupportedWindowModes);
|
||||
window.DisplaysChanged += onDisplaysChanged;
|
||||
}
|
||||
|
||||
if (host.Renderer is IWindowsRenderer windowsRenderer)
|
||||
@ -87,7 +93,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
displayDropdown = new DisplaySettingsDropdown
|
||||
{
|
||||
LabelText = GraphicsSettingsStrings.Display,
|
||||
Items = host.Window?.Displays,
|
||||
Items = window?.Displays,
|
||||
Current = currentDisplay,
|
||||
},
|
||||
resolutionDropdown = new ResolutionSettingsDropdown
|
||||
@ -97,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,
|
||||
@ -162,7 +173,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
|
||||
windowModeDropdown.Current.BindValueChanged(_ =>
|
||||
{
|
||||
updateDisplayModeDropdowns();
|
||||
updateDisplaySettingsVisibility();
|
||||
updateScreenModeWarning();
|
||||
}, true);
|
||||
|
||||
@ -187,7 +198,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
.Distinct());
|
||||
}
|
||||
|
||||
updateDisplayModeDropdowns();
|
||||
updateDisplaySettingsVisibility();
|
||||
}), true);
|
||||
|
||||
scalingMode.BindValueChanged(_ =>
|
||||
@ -202,19 +213,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
// initial update bypasses transforms
|
||||
updateScalingModeVisibility();
|
||||
|
||||
void updateDisplayModeDropdowns()
|
||||
{
|
||||
if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen)
|
||||
resolutionDropdown.Show();
|
||||
else
|
||||
resolutionDropdown.Hide();
|
||||
|
||||
if (displayDropdown.Items.Count() > 1)
|
||||
displayDropdown.Show();
|
||||
else
|
||||
displayDropdown.Hide();
|
||||
}
|
||||
|
||||
void updateScalingModeVisibility()
|
||||
{
|
||||
if (scalingMode.Value == ScalingMode.Off)
|
||||
@ -225,6 +223,33 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
private void onDisplaysChanged(IEnumerable<Display> displays)
|
||||
{
|
||||
Scheduler.AddOnce(d =>
|
||||
{
|
||||
displayDropdown.Items = d;
|
||||
updateDisplaySettingsVisibility();
|
||||
}, displays);
|
||||
}
|
||||
|
||||
private void updateDisplaySettingsVisibility()
|
||||
{
|
||||
if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen)
|
||||
resolutionDropdown.Show();
|
||||
else
|
||||
resolutionDropdown.Hide();
|
||||
|
||||
if (displayDropdown.Items.Count() > 1)
|
||||
displayDropdown.Show();
|
||||
else
|
||||
displayDropdown.Hide();
|
||||
|
||||
if (host.Window?.SafeAreaPadding.Value.Total != Vector2.Zero)
|
||||
safeAreaConsiderationsCheckbox.Show();
|
||||
else
|
||||
safeAreaConsiderationsCheckbox.Hide();
|
||||
}
|
||||
|
||||
private void updateScreenModeWarning()
|
||||
{
|
||||
if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS)
|
||||
@ -280,7 +305,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
};
|
||||
}
|
||||
|
||||
private Drawable preview;
|
||||
private Drawable? preview;
|
||||
|
||||
private void showPreview()
|
||||
{
|
||||
@ -291,6 +316,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
preview.Expire();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
if (window != null)
|
||||
window.DisplaysChanged -= onDisplaysChanged;
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
|
||||
private class ScalingPreview : ScalingContainer
|
||||
{
|
||||
public ScalingPreview()
|
||||
|
@ -327,6 +327,50 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
finalise();
|
||||
}
|
||||
|
||||
protected override bool OnTabletAuxiliaryButtonPress(TabletAuxiliaryButtonPressEvent e)
|
||||
{
|
||||
if (!HasFocus)
|
||||
return false;
|
||||
|
||||
bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState));
|
||||
finalise();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnTabletAuxiliaryButtonRelease(TabletAuxiliaryButtonReleaseEvent e)
|
||||
{
|
||||
if (!HasFocus)
|
||||
{
|
||||
base.OnTabletAuxiliaryButtonRelease(e);
|
||||
return;
|
||||
}
|
||||
|
||||
finalise();
|
||||
}
|
||||
|
||||
protected override bool OnTabletPenButtonPress(TabletPenButtonPressEvent e)
|
||||
{
|
||||
if (!HasFocus)
|
||||
return false;
|
||||
|
||||
bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState));
|
||||
finalise();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnTabletPenButtonRelease(TabletPenButtonReleaseEvent e)
|
||||
{
|
||||
if (!HasFocus)
|
||||
{
|
||||
base.OnTabletPenButtonRelease(e);
|
||||
return;
|
||||
}
|
||||
|
||||
finalise();
|
||||
}
|
||||
|
||||
private void clear()
|
||||
{
|
||||
if (bindTarget == null)
|
||||
@ -387,14 +431,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
if (bindTarget != null) bindTarget.IsBinding = true;
|
||||
}
|
||||
|
||||
private void updateStoreFromButton(KeyButton button)
|
||||
{
|
||||
realm.Run(r =>
|
||||
{
|
||||
var binding = r.Find<RealmKeyBinding>(((IHasGuidPrimaryKey)button.KeyBinding).ID);
|
||||
r.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString);
|
||||
});
|
||||
}
|
||||
private void updateStoreFromButton(KeyButton button) =>
|
||||
realm.WriteAsync(r => r.Find<RealmKeyBinding>(button.KeyBinding.ID).KeyCombinationString = button.KeyBinding.KeyCombinationString);
|
||||
|
||||
private void updateIsDefaultValue()
|
||||
{
|
||||
|
@ -72,7 +72,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
private void load(OsuColour colours, LocalisationManager localisation)
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -110,11 +110,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows || RuntimeInfo.OS == RuntimeInfo.Platform.Linux)
|
||||
{
|
||||
t.NewLine();
|
||||
t.AddText("If your tablet is not detected, please read ");
|
||||
t.AddLink("this FAQ", LinkAction.External, RuntimeInfo.OS == RuntimeInfo.Platform.Windows
|
||||
var formattedSource = MessageFormatter.FormatText(localisation.GetLocalisedBindableString(TabletSettingsStrings.NoTabletDetectedDescription(RuntimeInfo.OS == RuntimeInfo.Platform.Windows
|
||||
? @"https://opentabletdriver.net/Wiki/FAQ/Windows"
|
||||
: @"https://opentabletdriver.net/Wiki/FAQ/Linux");
|
||||
t.AddText(" for troubleshooting steps.");
|
||||
: @"https://opentabletdriver.net/Wiki/FAQ/Linux")).Value);
|
||||
t.AddLinks(formattedSource.Text, formattedSource.Links);
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
public class BeatmapSettings : SettingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => "Beatmaps";
|
||||
protected override LocalisableString Header => CommonStrings.Beatmaps;
|
||||
|
||||
private SettingsButton importBeatmapsButton = null!;
|
||||
private SettingsButton deleteBeatmapsButton = null!;
|
||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
public class CollectionsSettings : SettingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => "Collections";
|
||||
protected override LocalisableString Header => CommonStrings.Collections;
|
||||
|
||||
private SettingsButton importCollectionsButton = null!;
|
||||
|
||||
@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
private void deleteAllCollections()
|
||||
{
|
||||
realm.Write(r => r.RemoveAll<BeatmapCollection>());
|
||||
notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Deleted all collections!" });
|
||||
notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.DeletedAllCollections });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ using osu.Framework.Screens;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Screens;
|
||||
using osuTK;
|
||||
@ -71,14 +72,14 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "Migration in progress",
|
||||
Text = MaintenanceSettingsStrings.MigrationInProgress,
|
||||
Font = OsuFont.Default.With(size: 40)
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "This could take a few minutes depending on the speed of your disk(s).",
|
||||
Text = MaintenanceSettingsStrings.MigrationDescription,
|
||||
Font = OsuFont.Default.With(size: 30)
|
||||
},
|
||||
new LoadingSpinner(true)
|
||||
@ -89,7 +90,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "Please avoid interacting with the game!",
|
||||
Text = MaintenanceSettingsStrings.ProhibitedInteractDuringMigration,
|
||||
Font = OsuFont.Default.With(size: 30)
|
||||
},
|
||||
}
|
||||
@ -111,7 +112,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
notifications.Post(new SimpleNotification
|
||||
{
|
||||
Text = "Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up.",
|
||||
Text = MaintenanceSettingsStrings.FailedCleanupNotification,
|
||||
Activated = () =>
|
||||
{
|
||||
originalStorage.PresentExternally();
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
@ -35,7 +36,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
|
||||
public override bool HideOverlaysOnEnter => true;
|
||||
|
||||
public override LocalisableString HeaderText => "Please select a new location";
|
||||
public override LocalisableString HeaderText => MaintenanceSettingsStrings.SelectNewLocation;
|
||||
|
||||
protected override void OnSelection(DirectoryInfo directory)
|
||||
{
|
||||
@ -51,9 +52,9 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
// Quick test for whether there's already an osu! install at the target path.
|
||||
if (fileInfos.Any(f => f.Name == OsuGameBase.CLIENT_DATABASE_FILENAME))
|
||||
{
|
||||
dialogOverlay.Push(new ConfirmDialog("The target directory already seems to have an osu! install. Use that data instead?", () =>
|
||||
dialogOverlay.Push(new ConfirmDialog(MaintenanceSettingsStrings.TargetDirectoryAlreadyInstalledOsu, () =>
|
||||
{
|
||||
dialogOverlay.Push(new ConfirmDialog("To complete this operation, osu! will close. Please open it again to use the new data location.", () =>
|
||||
dialogOverlay.Push(new ConfirmDialog(MaintenanceSettingsStrings.RestartAndReOpenRequiredForCompletion, () =>
|
||||
{
|
||||
(storage as OsuStorage)?.ChangeDataPath(target.FullName);
|
||||
game.Exit();
|
||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
public class ModPresetSettings : SettingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => "Mod presets";
|
||||
protected override LocalisableString Header => CommonStrings.ModPresets;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
deleteAllButton.Enabled.Value = true;
|
||||
|
||||
if (deletionTask.IsCompletedSuccessfully)
|
||||
notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Deleted all mod presets!" });
|
||||
notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.DeletedAllModPresets });
|
||||
else if (deletionTask.IsFaulted)
|
||||
Logger.Error(deletionTask.Exception, "Failed to delete all mod presets");
|
||||
}
|
||||
@ -81,7 +81,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
undeleteButton.Enabled.Value = true;
|
||||
|
||||
if (undeletionTask.IsCompletedSuccessfully)
|
||||
notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Restored all deleted mod presets!" });
|
||||
notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.RestoredAllDeletedModPresets });
|
||||
else if (undeletionTask.IsFaulted)
|
||||
Logger.Error(undeletionTask.Exception, "Failed to restore mod presets");
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
public class ScoreSettings : SettingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => "Scores";
|
||||
protected override LocalisableString Header => CommonStrings.Scores;
|
||||
|
||||
private SettingsButton importScoresButton = null!;
|
||||
private SettingsButton deleteScoresButton = null!;
|
||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
public class SkinSettings : SettingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => "Skins";
|
||||
protected override LocalisableString Header => CommonStrings.Skins;
|
||||
|
||||
private SettingsButton importSkinsButton = null!;
|
||||
private SettingsButton deleteSkinsButton = null!;
|
||||
|
@ -16,6 +16,11 @@ namespace osu.Game.Overlays.Settings
|
||||
[Resolved]
|
||||
protected OverlayColourProvider ColourProvider { get; private set; }
|
||||
|
||||
protected SidebarButton()
|
||||
: base(HoverSampleSet.ButtonSidebar)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
|
@ -1,8 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
@ -23,25 +22,38 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
public class SettingsToolboxGroup : Container, IExpandable
|
||||
{
|
||||
private readonly string title;
|
||||
public const int CONTAINER_WIDTH = 270;
|
||||
|
||||
private const float transition_duration = 250;
|
||||
private const int border_thickness = 2;
|
||||
private const int header_height = 30;
|
||||
private const int corner_radius = 5;
|
||||
|
||||
private const float fade_duration = 800;
|
||||
private const float inactive_alpha = 0.5f;
|
||||
|
||||
private readonly Cached headerTextVisibilityCache = new Cached();
|
||||
|
||||
private readonly FillFlowContainer content;
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
private readonly FillFlowContainer content = new FillFlowContainer
|
||||
{
|
||||
Name = @"Content",
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Direction = FillDirection.Vertical,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Horizontal = 10, Top = 5, Bottom = 10 },
|
||||
Spacing = new Vector2(0, 15),
|
||||
};
|
||||
|
||||
public BindableBool Expanded { get; } = new BindableBool(true);
|
||||
|
||||
private readonly OsuSpriteText headerText;
|
||||
private OsuSpriteText headerText = null!;
|
||||
|
||||
private readonly Container headerContent;
|
||||
private Container headerContent = null!;
|
||||
|
||||
private Box background = null!;
|
||||
|
||||
private IconButton expandButton = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance.
|
||||
@ -49,20 +61,25 @@ namespace osu.Game.Overlays
|
||||
/// <param name="title">The title to be displayed in the header of this group.</param>
|
||||
public SettingsToolboxGroup(string title)
|
||||
{
|
||||
this.title = title;
|
||||
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Width = CONTAINER_WIDTH;
|
||||
Masking = true;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OverlayColourProvider? colourProvider)
|
||||
{
|
||||
CornerRadius = corner_radius;
|
||||
BorderColour = Color4.Black;
|
||||
BorderThickness = border_thickness;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black,
|
||||
Alpha = 0.5f,
|
||||
Alpha = 0.1f,
|
||||
Colour = colourProvider?.Background4 ?? Color4.Black,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
@ -88,7 +105,7 @@ namespace osu.Game.Overlays
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17),
|
||||
Padding = new MarginPadding { Left = 10, Right = 30 },
|
||||
},
|
||||
new IconButton
|
||||
expandButton = new IconButton
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.CentreRight,
|
||||
@ -99,19 +116,7 @@ namespace osu.Game.Overlays
|
||||
},
|
||||
}
|
||||
},
|
||||
content = new FillFlowContainer
|
||||
{
|
||||
Name = @"Content",
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Direction = FillDirection.Vertical,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeDuration = transition_duration,
|
||||
AutoSizeEasing = Easing.OutQuint,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding(15),
|
||||
Spacing = new Vector2(0, 15),
|
||||
}
|
||||
content
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -175,9 +180,10 @@ namespace osu.Game.Overlays
|
||||
|
||||
private void updateFadeState()
|
||||
{
|
||||
this.FadeTo(IsHovered ? 1 : inactive_alpha, fade_duration, Easing.OutQuint);
|
||||
}
|
||||
const float fade_duration = 500;
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
background.FadeTo(IsHovered ? 1 : 0.1f, fade_duration, Easing.OutQuint);
|
||||
expandButton.FadeTo(IsHovered ? 1 : 0, fade_duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using Markdig.Extensions.CustomContainers;
|
||||
using Markdig.Extensions.Yaml;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
@ -16,6 +17,7 @@ namespace osu.Game.Overlays.Wiki.Markdown
|
||||
public class WikiMarkdownContainer : OsuMarkdownContainer
|
||||
{
|
||||
protected override bool Footnotes => true;
|
||||
protected override bool CustomContainers => true;
|
||||
|
||||
public string CurrentPath
|
||||
{
|
||||
@ -26,6 +28,11 @@ namespace osu.Game.Overlays.Wiki.Markdown
|
||||
{
|
||||
switch (markdownObject)
|
||||
{
|
||||
case CustomContainer:
|
||||
// infoboxes are parsed into CustomContainer objects, but we don't have support for infoboxes yet.
|
||||
// todo: add support for infobox.
|
||||
break;
|
||||
|
||||
case YamlFrontMatterBlock yamlFrontMatterBlock:
|
||||
container.Add(new WikiNoticeContainer(yamlFrontMatterBlock));
|
||||
break;
|
||||
|
@ -46,6 +46,10 @@ namespace osu.Game.Replays.Legacy
|
||||
[IgnoreMember]
|
||||
public bool MouseRight2 => ButtonState.HasFlagFast(ReplayButtonState.Right2);
|
||||
|
||||
[JsonIgnore]
|
||||
[IgnoreMember]
|
||||
public bool Smoke => ButtonState.HasFlagFast(ReplayButtonState.Smoke);
|
||||
|
||||
[Key(3)]
|
||||
public ReplayButtonState ButtonState;
|
||||
|
||||
|
@ -3,14 +3,21 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.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;
|
||||
@ -18,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
|
||||
{
|
||||
@ -30,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,
|
||||
@ -42,35 +50,114 @@ 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)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
AddInternal(RightSideToolboxContainer = new ExpandingToolboxContainer(130, 250)
|
||||
AddInternal(new Container
|
||||
{
|
||||
Padding = new MarginPadding(10),
|
||||
Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Child = new EditorToolboxGroup("snapping")
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Child = distanceSpacingSlider = new ExpandableSlider<double, SizeSlider<double>>
|
||||
new Box
|
||||
{
|
||||
Current = { BindTarget = DistanceSpacingMultiplier },
|
||||
KeyboardStep = adjust_step,
|
||||
Colour = colourProvider.Background5,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
RightSideToolboxContainer = new ExpandingToolboxContainer(130, 250)
|
||||
{
|
||||
Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
|
||||
Child = new EditorToolboxGroup("snapping")
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
@ -88,22 +175,61 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.EditorIncreaseDistanceSpacing:
|
||||
case GlobalAction.EditorDecreaseDistanceSpacing:
|
||||
return adjustDistanceSpacing(e.Action, adjust_step);
|
||||
return AdjustDistanceSpacing(e.Action, adjust_step);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
public virtual void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
}
|
||||
|
||||
@ -113,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;
|
||||
@ -129,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)
|
||||
{
|
||||
return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * referenceObject.DifficultyControlPoint.SliderVelocity / BeatSnapProvider.BeatDivisor);
|
||||
return (float)(100 * 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -19,7 +19,8 @@ namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
|
||||
FillFlow.Spacing = new Vector2(10);
|
||||
FillFlow.Spacing = new Vector2(5);
|
||||
Padding = new MarginPadding { Vertical = 5 };
|
||||
}
|
||||
|
||||
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && anyToolboxHovered(screenSpacePos);
|
||||
|
@ -12,10 +12,12 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Configuration;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -80,7 +82,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
Config = Dependencies.Get<IRulesetConfigCache>().GetConfigFor(Ruleset);
|
||||
|
||||
@ -102,7 +104,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
PlayfieldContentContainer = new Container
|
||||
{
|
||||
Name = "Content",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -116,25 +118,37 @@ namespace osu.Game.Rulesets.Edit
|
||||
.WithChild(BlueprintContainer = CreateBlueprintContainer())
|
||||
}
|
||||
},
|
||||
new ExpandingToolboxContainer(90, 200)
|
||||
new Container
|
||||
{
|
||||
Padding = new MarginPadding(10),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new EditorToolboxGroup("toolbox (1-9)")
|
||||
new Box
|
||||
{
|
||||
Child = toolboxCollection = new EditorRadioButtonCollection { RelativeSizeAxes = Axes.X }
|
||||
Colour = colourProvider.Background5,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new EditorToolboxGroup("toggles (Q~P)")
|
||||
new ExpandingToolboxContainer(60, 200)
|
||||
{
|
||||
Child = togglesCollection = new FillFlowContainer
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 5),
|
||||
},
|
||||
}
|
||||
new EditorToolboxGroup("toolbox (1-9)")
|
||||
{
|
||||
Child = toolboxCollection = new EditorRadioButtonCollection { RelativeSizeAxes = Axes.X }
|
||||
},
|
||||
new EditorToolboxGroup("toggles (Q~P)")
|
||||
{
|
||||
Child = togglesCollection = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 5),
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -152,6 +166,15 @@ namespace osu.Game.Rulesets.Edit
|
||||
EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Houses all content relevant to the playfield.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Generally implementations should not be adding to this directly.
|
||||
/// Use <see cref="LayerBelowRuleset"/> or <see cref="BlueprintContainer"/> instead.
|
||||
/// </remarks>
|
||||
protected Container PlayfieldContentContainer { get; private set; }
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@ -215,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))
|
||||
|
@ -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" });
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -12,7 +11,6 @@ using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.OpenGL.Vertices;
|
||||
@ -20,6 +18,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -84,8 +83,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
flashlight.Combo.BindTo(Combo);
|
||||
drawableRuleset.KeyBindingInputManager.Add(flashlight);
|
||||
|
||||
flashlight.Breaks = drawableRuleset.Beatmap.Breaks;
|
||||
}
|
||||
|
||||
protected abstract Flashlight CreateFlashlight();
|
||||
@ -100,8 +97,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override bool RemoveCompletedTransforms => false;
|
||||
|
||||
public List<BreakPeriod> Breaks = new List<BreakPeriod>();
|
||||
|
||||
private readonly float defaultFlashlightSize;
|
||||
private readonly float sizeMultiplier;
|
||||
private readonly bool comboBasedSize;
|
||||
@ -119,46 +114,50 @@ namespace osu.Game.Rulesets.Mods
|
||||
shader = shaderManager.Load("PositionAndColour", FragmentShader);
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private Player? player { get; set; }
|
||||
|
||||
private readonly IBindable<bool> isBreakTime = new BindableBool();
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Combo.ValueChanged += OnComboChange;
|
||||
Combo.ValueChanged += _ => UpdateFlashlightSize(GetSize());
|
||||
|
||||
using (BeginAbsoluteSequence(0))
|
||||
if (player != null)
|
||||
{
|
||||
foreach (var breakPeriod in Breaks)
|
||||
{
|
||||
if (!breakPeriod.HasEffect)
|
||||
continue;
|
||||
|
||||
if (breakPeriod.Duration < FLASHLIGHT_FADE_DURATION * 2) continue;
|
||||
|
||||
this.Delay(breakPeriod.StartTime + FLASHLIGHT_FADE_DURATION).FadeOutFromOne(FLASHLIGHT_FADE_DURATION);
|
||||
this.Delay(breakPeriod.EndTime - FLASHLIGHT_FADE_DURATION).FadeInFromZero(FLASHLIGHT_FADE_DURATION);
|
||||
}
|
||||
isBreakTime.BindTo(player.IsBreakTime);
|
||||
isBreakTime.BindValueChanged(_ => UpdateFlashlightSize(GetSize()), true);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void OnComboChange(ValueChangedEvent<int> e);
|
||||
protected abstract void UpdateFlashlightSize(float size);
|
||||
|
||||
protected abstract string FragmentShader { get; }
|
||||
|
||||
protected float GetSizeFor(int combo)
|
||||
protected float GetSize()
|
||||
{
|
||||
float size = defaultFlashlightSize * sizeMultiplier;
|
||||
|
||||
if (comboBasedSize)
|
||||
{
|
||||
if (combo >= 200)
|
||||
size *= 0.8f;
|
||||
else if (combo >= 100)
|
||||
size *= 0.9f;
|
||||
}
|
||||
if (isBreakTime.Value)
|
||||
size *= 2.5f;
|
||||
else if (comboBasedSize)
|
||||
size *= GetComboScaleFor(Combo.Value);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
protected virtual float GetComboScaleFor(int combo)
|
||||
{
|
||||
if (combo >= 200)
|
||||
return 0.625f;
|
||||
if (combo >= 100)
|
||||
return 0.8125f;
|
||||
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
private Vector2 flashlightPosition;
|
||||
|
||||
protected Vector2 FlashlightPosition
|
||||
@ -201,6 +200,20 @@ namespace osu.Game.Rulesets.Mods
|
||||
}
|
||||
}
|
||||
|
||||
private float flashlightSmoothness = 1.1f;
|
||||
|
||||
public float FlashlightSmoothness
|
||||
{
|
||||
get => flashlightSmoothness;
|
||||
set
|
||||
{
|
||||
if (flashlightSmoothness == value) return;
|
||||
|
||||
flashlightSmoothness = value;
|
||||
Invalidate(Invalidation.DrawNode);
|
||||
}
|
||||
}
|
||||
|
||||
private class FlashlightDrawNode : DrawNode
|
||||
{
|
||||
protected new Flashlight Source => (Flashlight)base.Source;
|
||||
@ -210,6 +223,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
private Vector2 flashlightPosition;
|
||||
private Vector2 flashlightSize;
|
||||
private float flashlightDim;
|
||||
private float flashlightSmoothness;
|
||||
|
||||
private IVertexBatch<PositionAndColourVertex>? quadBatch;
|
||||
private Action<TexturedVertex2D>? addAction;
|
||||
@ -228,6 +242,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
flashlightPosition = Vector2Extensions.Transform(Source.FlashlightPosition, DrawInfo.Matrix);
|
||||
flashlightSize = Source.FlashlightSize * DrawInfo.Matrix.ExtractScale().Xy;
|
||||
flashlightDim = Source.FlashlightDim;
|
||||
flashlightSmoothness = Source.flashlightSmoothness;
|
||||
}
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
@ -249,6 +264,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
shader.GetUniform<Vector2>("flashlightPos").UpdateValue(ref flashlightPosition);
|
||||
shader.GetUniform<Vector2>("flashlightSize").UpdateValue(ref flashlightSize);
|
||||
shader.GetUniform<float>("flashlightDim").UpdateValue(ref flashlightDim);
|
||||
shader.GetUniform<float>("flashlightSmoothness").UpdateValue(ref flashlightSmoothness);
|
||||
|
||||
renderer.DrawQuad(renderer.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction: addAction);
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -40,14 +38,21 @@ namespace osu.Game.Rulesets.Objects
|
||||
for (int i = 0; i < timingPoints.Count; i++)
|
||||
{
|
||||
TimingControlPoint currentTimingPoint = timingPoints[i];
|
||||
EffectControlPoint currentEffectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTimingPoint.Time);
|
||||
int currentBeat = 0;
|
||||
|
||||
// Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object
|
||||
double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - currentTimingPoint.BeatLength : lastHitTime + currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator;
|
||||
// Stop on the next timing point, or if there is no next timing point stop slightly past the last object
|
||||
double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time : lastHitTime + currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator;
|
||||
|
||||
double startTime = currentTimingPoint.Time;
|
||||
double barLength = currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator;
|
||||
|
||||
for (double t = currentTimingPoint.Time; Precision.DefinitelyBigger(endTime, t); t += barLength, currentBeat++)
|
||||
if (currentEffectPoint.OmitFirstBarLine)
|
||||
{
|
||||
startTime += barLength;
|
||||
}
|
||||
|
||||
for (double t = startTime; Precision.AlmostBigger(endTime, t); t += barLength, currentBeat++)
|
||||
{
|
||||
double roundedTime = Math.Round(t, MidpointRounding.AwayFromZero);
|
||||
|
||||
|
@ -204,18 +204,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"/>.
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
@ -198,6 +199,21 @@ namespace osu.Game.Rulesets.Objects
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
protected virtual HitWindows CreateHitWindows() => new HitWindows();
|
||||
|
||||
public IList<HitSampleInfo> CreateSlidingSamples()
|
||||
{
|
||||
var slidingSamples = new List<HitSampleInfo>();
|
||||
|
||||
var normalSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
|
||||
if (normalSample != null)
|
||||
slidingSamples.Add(normalSample.With("sliderslide"));
|
||||
|
||||
var whistleSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE);
|
||||
if (whistleSample != null)
|
||||
slidingSamples.Add(whistleSample.With("sliderwhistle"));
|
||||
|
||||
return slidingSamples;
|
||||
}
|
||||
}
|
||||
|
||||
public static class HitObjectExtensions
|
||||
|
@ -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}");
|
||||
}
|
||||
|
||||
|
@ -11,12 +11,12 @@ namespace osu.Game.Rulesets.Timing
|
||||
/// <summary>
|
||||
/// A control point which adds an aggregated multiplier based on the provided <see cref="TimingPoint"/>'s BeatLength and <see cref="EffectPoint"/>'s SpeedMultiplier.
|
||||
/// </summary>
|
||||
public class MultiplierControlPoint : IComparable<MultiplierControlPoint>
|
||||
public class MultiplierControlPoint : IComparable<MultiplierControlPoint>, IControlPoint
|
||||
{
|
||||
/// <summary>
|
||||
/// The time in milliseconds at which this <see cref="MultiplierControlPoint"/> starts.
|
||||
/// </summary>
|
||||
public double StartTime;
|
||||
public double Time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The aggregate multiplier which this <see cref="MultiplierControlPoint"/> provides.
|
||||
@ -54,13 +54,13 @@ namespace osu.Game.Rulesets.Timing
|
||||
/// <summary>
|
||||
/// Creates a <see cref="MultiplierControlPoint"/>.
|
||||
/// </summary>
|
||||
/// <param name="startTime">The start time of this <see cref="MultiplierControlPoint"/>.</param>
|
||||
public MultiplierControlPoint(double startTime)
|
||||
/// <param name="time">The start time of this <see cref="MultiplierControlPoint"/>.</param>
|
||||
public MultiplierControlPoint(double time)
|
||||
{
|
||||
StartTime = startTime;
|
||||
Time = time;
|
||||
}
|
||||
|
||||
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
|
||||
public int CompareTo(MultiplierControlPoint other) => StartTime.CompareTo(other?.StartTime);
|
||||
public int CompareTo(MultiplierControlPoint other) => Time.CompareTo(other?.Time);
|
||||
}
|
||||
}
|
||||
|
@ -384,7 +384,7 @@ namespace osu.Game.Rulesets.UI
|
||||
// only show the cursor when within the playfield, by default.
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Playfield.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
CursorContainer IProvideCursor.MenuCursor => Playfield.Cursor;
|
||||
CursorContainer IProvideCursor.Cursor => Playfield.Cursor;
|
||||
|
||||
public override GameplayCursorContainer Cursor => Playfield.Cursor;
|
||||
|
||||
@ -499,6 +499,7 @@ namespace osu.Game.Rulesets.UI
|
||||
/// <summary>
|
||||
/// The cursor being displayed by the <see cref="Playfield"/>. May be null if no cursor is provided.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public abstract GameplayCursorContainer Cursor { get; }
|
||||
|
||||
/// <summary>
|
||||
|
@ -202,16 +202,14 @@ namespace osu.Game.Rulesets.UI
|
||||
/// <summary>
|
||||
/// The cursor currently being used by this <see cref="Playfield"/>. May be null if no cursor is provided.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public GameplayCursorContainer Cursor { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Provide a cursor which is to be used for gameplay.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The default provided cursor is invisible when inside the bounds of the <see cref="Playfield"/>.
|
||||
/// </remarks>
|
||||
/// <returns>The cursor, or null to show the menu cursor.</returns>
|
||||
protected virtual GameplayCursorContainer CreateCursor() => new InvisibleCursorContainer();
|
||||
protected virtual GameplayCursorContainer CreateCursor() => null;
|
||||
|
||||
/// <summary>
|
||||
/// Registers a <see cref="Playfield"/> as a nested <see cref="Playfield"/>.
|
||||
@ -522,14 +520,5 @@ namespace osu.Game.Rulesets.UI
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public class InvisibleCursorContainer : GameplayCursorContainer
|
||||
{
|
||||
protected override Drawable CreateCursor() => new InvisibleCursor();
|
||||
|
||||
private class InvisibleCursor : Drawable
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -230,9 +230,9 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
}
|
||||
|
||||
protected override void ReloadMappings()
|
||||
protected override void ReloadMappings(IQueryable<RealmKeyBinding> realmKeyBindings)
|
||||
{
|
||||
base.ReloadMappings();
|
||||
base.ReloadMappings(realmKeyBindings);
|
||||
|
||||
KeyBindings = KeyBindings.Where(b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
return -PositionAt(startTime, endTime, timeRange, scrollLength);
|
||||
}
|
||||
|
||||
public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
|
||||
public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)
|
||||
=> (float)((time - currentTime) / timeRange * scrollLength);
|
||||
|
||||
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
|
||||
|
@ -53,8 +53,9 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
/// <param name="currentTime">The current time.</param>
|
||||
/// <param name="timeRange">The amount of visible time.</param>
|
||||
/// <param name="scrollLength">The absolute spatial length through <paramref name="timeRange"/>.</param>
|
||||
/// <param name="originTime">The time to be used for control point lookups (ie. the parent's start time for nested hit objects).</param>
|
||||
/// <returns>The absolute spatial position.</returns>
|
||||
float PositionAt(double time, double currentTime, double timeRange, float scrollLength);
|
||||
float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the time which brings a point to a provided spatial position given the current time.
|
||||
@ -63,7 +64,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
/// <param name="currentTime">The current time.</param>
|
||||
/// <param name="timeRange">The amount of visible time.</param>
|
||||
/// <param name="scrollLength">The absolute spatial length through <paramref name="timeRange"/>.</param>
|
||||
/// <returns>The time at which <see cref="PositionAt(double,double,double,float)"/> == <paramref name="position"/>.</returns>
|
||||
/// <returns>The time at which <see cref="PositionAt(double,double,double,float, double?)"/> == <paramref name="position"/>.</returns>
|
||||
double TimeAt(float position, double currentTime, double timeRange, float scrollLength);
|
||||
|
||||
/// <summary>
|
||||
|
@ -4,22 +4,20 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
{
|
||||
public class OverlappingScrollAlgorithm : IScrollAlgorithm
|
||||
{
|
||||
private readonly MultiplierControlPoint searchPoint;
|
||||
|
||||
private readonly SortedList<MultiplierControlPoint> controlPoints;
|
||||
|
||||
public OverlappingScrollAlgorithm(SortedList<MultiplierControlPoint> controlPoints)
|
||||
{
|
||||
this.controlPoints = controlPoints;
|
||||
|
||||
searchPoint = new MultiplierControlPoint();
|
||||
}
|
||||
|
||||
public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)
|
||||
@ -37,8 +35,8 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
return -PositionAt(startTime, endTime, timeRange, scrollLength);
|
||||
}
|
||||
|
||||
public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
|
||||
=> (float)((time - currentTime) / timeRange * controlPointAt(time).Multiplier * scrollLength);
|
||||
public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)
|
||||
=> (float)((time - currentTime) / timeRange * controlPointAt(originTime ?? time).Multiplier * scrollLength);
|
||||
|
||||
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
|
||||
{
|
||||
@ -52,7 +50,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
for (; i < controlPoints.Count; i++)
|
||||
{
|
||||
float lastPos = pos;
|
||||
pos = PositionAt(controlPoints[i].StartTime, currentTime, timeRange, scrollLength);
|
||||
pos = PositionAt(controlPoints[i].Time, currentTime, timeRange, scrollLength);
|
||||
|
||||
if (pos > position)
|
||||
{
|
||||
@ -64,7 +62,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
|
||||
i = Math.Clamp(i, 0, controlPoints.Count - 1);
|
||||
|
||||
return controlPoints[i].StartTime + (position - pos) * timeRange / controlPoints[i].Multiplier / scrollLength;
|
||||
return controlPoints[i].Time + (position - pos) * timeRange / controlPoints[i].Multiplier / scrollLength;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
@ -78,19 +76,11 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
/// <returns>The <see cref="MultiplierControlPoint"/>.</returns>
|
||||
private MultiplierControlPoint controlPointAt(double time)
|
||||
{
|
||||
if (controlPoints.Count == 0)
|
||||
return new MultiplierControlPoint(double.NegativeInfinity);
|
||||
|
||||
if (time < controlPoints[0].StartTime)
|
||||
return controlPoints[0];
|
||||
|
||||
searchPoint.StartTime = time;
|
||||
int index = controlPoints.BinarySearch(searchPoint);
|
||||
|
||||
if (index < 0)
|
||||
index = ~index - 1;
|
||||
|
||||
return controlPoints[index];
|
||||
return ControlPointInfo.BinarySearch(controlPoints, time)
|
||||
// The standard binary search will fail if there's no control points, or if the time is before the first.
|
||||
// For this method, we want to use the first control point in the latter case.
|
||||
?? controlPoints.FirstOrDefault()
|
||||
?? new MultiplierControlPoint(double.NegativeInfinity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
return (float)(objectLength * scrollLength);
|
||||
}
|
||||
|
||||
public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
|
||||
public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)
|
||||
{
|
||||
double timelineLength = relativePositionAt(time, timeRange) - relativePositionAt(currentTime, timeRange);
|
||||
return (float)(timelineLength * scrollLength);
|
||||
@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
if (controlPoints.Count == 0)
|
||||
return;
|
||||
|
||||
positionMappings.Add(new PositionMapping(controlPoints[0].StartTime, controlPoints[0]));
|
||||
positionMappings.Add(new PositionMapping(controlPoints[0].Time, controlPoints[0]));
|
||||
|
||||
for (int i = 0; i < controlPoints.Count - 1; i++)
|
||||
{
|
||||
@ -129,9 +129,9 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
var next = controlPoints[i + 1];
|
||||
|
||||
// Figure out how much of the time range the duration represents, and adjust it by the speed multiplier
|
||||
float length = (float)((next.StartTime - current.StartTime) / timeRange * current.Multiplier);
|
||||
float length = (float)((next.Time - current.Time) / timeRange * current.Multiplier);
|
||||
|
||||
positionMappings.Add(new PositionMapping(next.StartTime, next, positionMappings[^1].Position + length));
|
||||
positionMappings.Add(new PositionMapping(next.Time, next, positionMappings[^1].Position + length));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,9 +158,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
// Trim unwanted sequences of timing changes
|
||||
timingChanges = timingChanges
|
||||
// Collapse sections after the last hit object
|
||||
.Where(s => s.StartTime <= lastObjectTime)
|
||||
.Where(s => s.Time <= lastObjectTime)
|
||||
// Collapse sections with the same start time
|
||||
.GroupBy(s => s.StartTime).Select(g => g.Last()).OrderBy(s => s.StartTime);
|
||||
.GroupBy(s => s.Time).Select(g => g.Last()).OrderBy(s => s.Time);
|
||||
|
||||
ControlPoints.AddRange(timingChanges);
|
||||
|
||||
|
@ -5,10 +5,10 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@ -93,9 +93,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
/// <summary>
|
||||
/// Given a time, return the position along the scrolling axis within this <see cref="HitObjectContainer"/> at time <paramref name="currentTime"/>.
|
||||
/// </summary>
|
||||
public float PositionAtTime(double time, double currentTime)
|
||||
public float PositionAtTime(double time, double currentTime, double? originTime = null)
|
||||
{
|
||||
float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength);
|
||||
float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength, originTime);
|
||||
return axisInverted ? -scrollPosition : scrollPosition;
|
||||
}
|
||||
|
||||
@ -127,6 +127,16 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
|
||||
private float scrollLength => scrollingAxis == Direction.Horizontal ? DrawWidth : DrawHeight;
|
||||
|
||||
public override void Add(HitObjectLifetimeEntry entry)
|
||||
{
|
||||
// Scroll info is not available until loaded.
|
||||
// The lifetime of all entries will be updated in the first Update.
|
||||
if (IsLoaded)
|
||||
setComputedLifetimeStart(entry);
|
||||
|
||||
base.Add(entry);
|
||||
}
|
||||
|
||||
protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
|
||||
{
|
||||
base.AddDrawable(entry, drawable);
|
||||
@ -145,7 +155,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
|
||||
private void invalidateHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
|
||||
layoutComputed.Remove(hitObject);
|
||||
}
|
||||
|
||||
@ -157,10 +166,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
|
||||
layoutComputed.Clear();
|
||||
|
||||
// Reset lifetime to the conservative estimation.
|
||||
// If a drawable becomes alive by this lifetime, its lifetime will be updated to a more precise lifetime in the next update.
|
||||
foreach (var entry in Entries)
|
||||
entry.SetInitialLifetime();
|
||||
setComputedLifetimeStart(entry);
|
||||
|
||||
scrollingInfo.Algorithm.Reset();
|
||||
|
||||
@ -187,42 +194,52 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
}
|
||||
}
|
||||
|
||||
private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
|
||||
/// <summary>
|
||||
/// Get a conservative maximum bounding box of a <see cref="DrawableHitObject"/> corresponding to <paramref name="entry"/>.
|
||||
/// It is used to calculate when the hit object appears.
|
||||
/// </summary>
|
||||
protected virtual RectangleF GetConservativeBoundingBox(HitObjectLifetimeEntry entry) => new RectangleF().Inflate(100);
|
||||
|
||||
private double computeDisplayStartTime(HitObjectLifetimeEntry entry)
|
||||
{
|
||||
// Origin position may be relative to the parent size
|
||||
Debug.Assert(hitObject.Parent != null);
|
||||
RectangleF boundingBox = GetConservativeBoundingBox(entry);
|
||||
float startOffset = 0;
|
||||
|
||||
float originAdjustment = 0.0f;
|
||||
|
||||
// calculate the dimension of the part of the hitobject that should already be visible
|
||||
// when the hitobject origin first appears inside the scrolling container
|
||||
switch (direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
originAdjustment = hitObject.OriginPosition.Y;
|
||||
case ScrollingDirection.Right:
|
||||
startOffset = boundingBox.Right;
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Down:
|
||||
originAdjustment = hitObject.DrawHeight - hitObject.OriginPosition.Y;
|
||||
startOffset = boundingBox.Bottom;
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Left:
|
||||
originAdjustment = hitObject.OriginPosition.X;
|
||||
startOffset = -boundingBox.Left;
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Right:
|
||||
originAdjustment = hitObject.DrawWidth - hitObject.OriginPosition.X;
|
||||
case ScrollingDirection.Up:
|
||||
startOffset = -boundingBox.Top;
|
||||
break;
|
||||
}
|
||||
|
||||
double computedStartTime = scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength);
|
||||
|
||||
// always load the hitobject before its first judgement offset
|
||||
return Math.Min(hitObject.HitObject.StartTime - hitObject.MaximumJudgementOffset, computedStartTime);
|
||||
return scrollingInfo.Algorithm.GetDisplayStartTime(entry.HitObject.StartTime, startOffset, timeRange.Value, scrollLength);
|
||||
}
|
||||
|
||||
private void updateLayoutRecursive(DrawableHitObject hitObject)
|
||||
private void setComputedLifetimeStart(HitObjectLifetimeEntry entry)
|
||||
{
|
||||
double computedStartTime = computeDisplayStartTime(entry);
|
||||
|
||||
// always load the hitobject before its first judgement offset
|
||||
double judgementOffset = entry.HitObject.HitWindows?.WindowFor(Scoring.HitResult.Miss) ?? 0;
|
||||
entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - judgementOffset, computedStartTime);
|
||||
}
|
||||
|
||||
private void updateLayoutRecursive(DrawableHitObject hitObject, double? parentHitObjectStartTime = null)
|
||||
{
|
||||
parentHitObjectStartTime ??= hitObject.HitObject.StartTime;
|
||||
|
||||
if (hitObject.HitObject is IHasDuration e)
|
||||
{
|
||||
float length = LengthAtTime(hitObject.HitObject.StartTime, e.EndTime);
|
||||
@ -234,16 +251,17 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
|
||||
foreach (var obj in hitObject.NestedHitObjects)
|
||||
{
|
||||
updateLayoutRecursive(obj);
|
||||
updateLayoutRecursive(obj, parentHitObjectStartTime);
|
||||
|
||||
// Nested hitobjects don't need to scroll, but they do need accurate positions
|
||||
updatePosition(obj, hitObject.HitObject.StartTime);
|
||||
// Nested hitobjects don't need to scroll, but they do need accurate positions and start lifetime
|
||||
updatePosition(obj, hitObject.HitObject.StartTime, parentHitObjectStartTime);
|
||||
setComputedLifetimeStart(obj.Entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePosition(DrawableHitObject hitObject, double currentTime)
|
||||
private void updatePosition(DrawableHitObject hitObject, double currentTime, double? parentHitObjectStartTime = null)
|
||||
{
|
||||
float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime);
|
||||
float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime, parentHitObjectStartTime);
|
||||
|
||||
if (scrollingAxis == Direction.Horizontal)
|
||||
hitObject.X = position;
|
||||
|
@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
/// </summary>
|
||||
public virtual Vector2 ScreenSpacePositionAtTime(double time) => HitObjectContainer.ScreenSpacePositionAtTime(time);
|
||||
|
||||
protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer();
|
||||
protected sealed override HitObjectContainer CreateHitObjectContainer() => CreateScrollingHitObjectContainer();
|
||||
|
||||
protected virtual ScrollingHitObjectContainer CreateScrollingHitObjectContainer() => new ScrollingHitObjectContainer();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user