mirror of
https://github.com/osukey/osukey.git
synced 2025-08-04 15:16:38 +09:00
Merge branch 'master' into beatmap-cancellation-token
This commit is contained in:
@ -1,13 +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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Mixing;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -19,27 +14,26 @@ namespace osu.Game.Audio
|
||||
{
|
||||
public class PreviewTrackManager : Component
|
||||
{
|
||||
private readonly IAdjustableAudioComponent mainTrackAdjustments;
|
||||
|
||||
private readonly BindableDouble muteBindable = new BindableDouble();
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audio { get; set; }
|
||||
|
||||
private PreviewTrackStore trackStore;
|
||||
private ITrackStore trackStore;
|
||||
|
||||
protected TrackManagerPreviewTrack CurrentTrack;
|
||||
|
||||
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(OsuGameBase.GLOBAL_TRACK_VOLUME_ADJUST);
|
||||
public PreviewTrackManager(IAdjustableAudioComponent mainTrackAdjustments)
|
||||
{
|
||||
this.mainTrackAdjustments = mainTrackAdjustments;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audioManager)
|
||||
{
|
||||
// this is a temporary solution to get around muting ourselves.
|
||||
// todo: update this once we have a BackgroundTrackManager or similar.
|
||||
trackStore = new PreviewTrackStore(audioManager.TrackMixer, new OnlineStore());
|
||||
|
||||
audio.AddItem(trackStore);
|
||||
trackStore.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust);
|
||||
trackStore.AddAdjustment(AdjustableProperty.Volume, audio.VolumeTrack);
|
||||
trackStore = audioManager.GetTrackStore(new OnlineStore());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -55,7 +49,7 @@ namespace osu.Game.Audio
|
||||
{
|
||||
CurrentTrack?.Stop();
|
||||
CurrentTrack = track;
|
||||
audio.Tracks.AddAdjustment(AdjustableProperty.Volume, muteBindable);
|
||||
mainTrackAdjustments.AddAdjustment(AdjustableProperty.Volume, muteBindable);
|
||||
});
|
||||
|
||||
track.Stopped += () => Schedule(() =>
|
||||
@ -64,7 +58,7 @@ namespace osu.Game.Audio
|
||||
return;
|
||||
|
||||
CurrentTrack = null;
|
||||
audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
|
||||
mainTrackAdjustments.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
|
||||
});
|
||||
|
||||
return track;
|
||||
@ -116,52 +110,5 @@ namespace osu.Game.Audio
|
||||
|
||||
protected override Track GetTrack() => trackManager.Get($"https://b.ppy.sh/preview/{beatmapSetInfo.OnlineID}.mp3");
|
||||
}
|
||||
|
||||
private class PreviewTrackStore : AudioCollectionManager<AdjustableAudioComponent>, ITrackStore
|
||||
{
|
||||
private readonly AudioMixer defaultMixer;
|
||||
private readonly IResourceStore<byte[]> store;
|
||||
|
||||
internal PreviewTrackStore(AudioMixer defaultMixer, IResourceStore<byte[]> store)
|
||||
{
|
||||
this.defaultMixer = defaultMixer;
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
public Track GetVirtual(double length = double.PositiveInfinity)
|
||||
{
|
||||
if (IsDisposed) throw new ObjectDisposedException($"Cannot retrieve items for an already disposed {nameof(PreviewTrackStore)}");
|
||||
|
||||
var track = new TrackVirtual(length);
|
||||
AddItem(track);
|
||||
return track;
|
||||
}
|
||||
|
||||
public Track Get(string name)
|
||||
{
|
||||
if (IsDisposed) throw new ObjectDisposedException($"Cannot retrieve items for an already disposed {nameof(PreviewTrackStore)}");
|
||||
|
||||
if (string.IsNullOrEmpty(name)) return null;
|
||||
|
||||
var dataStream = store.GetStream(name);
|
||||
|
||||
if (dataStream == null)
|
||||
return null;
|
||||
|
||||
// Todo: This is quite unsafe. TrackBass shouldn't be exposed as public.
|
||||
Track track = new TrackBass(dataStream);
|
||||
|
||||
defaultMixer.Add(track);
|
||||
AddItem(track);
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
public Task<Track> GetAsync(string name) => Task.Run(() => Get(name));
|
||||
|
||||
public Stream GetStream(string name) => store.GetStream(name);
|
||||
|
||||
public IEnumerable<string> GetAvailableResources() => store.GetAvailableResources();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ namespace osu.Game.Beatmaps
|
||||
Title = @"Unknown",
|
||||
AuthorString = @"Unknown Creator",
|
||||
},
|
||||
Version = @"Normal",
|
||||
DifficultyName = @"Normal",
|
||||
BaseDifficulty = Difficulty,
|
||||
};
|
||||
}
|
||||
|
@ -291,7 +291,7 @@ namespace osu.Game.Beatmaps
|
||||
catch (BeatmapInvalidForRulesetException e)
|
||||
{
|
||||
if (rulesetInfo.Equals(beatmapInfo.Ruleset))
|
||||
Logger.Error(e, $"Failed to convert {beatmapInfo.OnlineBeatmapID} to the beatmap's default ruleset ({beatmapInfo.Ruleset}).");
|
||||
Logger.Error(e, $"Failed to convert {beatmapInfo.OnlineID} to the beatmap's default ruleset ({beatmapInfo.Ruleset}).");
|
||||
|
||||
return new StarDifficulty();
|
||||
}
|
||||
|
@ -23,13 +23,14 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public int BeatmapVersion;
|
||||
|
||||
private int? onlineBeatmapID;
|
||||
private int? onlineID;
|
||||
|
||||
[JsonProperty("id")]
|
||||
public int? OnlineBeatmapID
|
||||
[Column("OnlineBeatmapID")]
|
||||
public int? OnlineID
|
||||
{
|
||||
get => onlineBeatmapID;
|
||||
set => onlineBeatmapID = value > 0 ? value : null;
|
||||
get => onlineID;
|
||||
set => onlineID = value > 0 ? value : null;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
@ -134,12 +135,12 @@ namespace osu.Game.Beatmaps
|
||||
public double TimelineZoom { get; set; }
|
||||
|
||||
// Metadata
|
||||
public string Version { get; set; }
|
||||
|
||||
private string versionString => string.IsNullOrEmpty(Version) ? string.Empty : $"[{Version}]";
|
||||
[Column("Version")]
|
||||
public string DifficultyName { get; set; }
|
||||
|
||||
[JsonProperty("difficulty_rating")]
|
||||
public double StarDifficulty { get; set; }
|
||||
[Column("StarDifficulty")]
|
||||
public double StarRating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Currently only populated for beatmap deletion. Use <see cref="ScoreManager"/> to query scores.
|
||||
@ -147,7 +148,7 @@ namespace osu.Game.Beatmaps
|
||||
public List<ScoreInfo> Scores { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarDifficulty);
|
||||
public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarRating);
|
||||
|
||||
public override string ToString() => this.GetDisplayTitle();
|
||||
|
||||
@ -176,15 +177,12 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
#region Implementation of IHasOnlineID
|
||||
|
||||
public int OnlineID => OnlineBeatmapID ?? -1;
|
||||
int IHasOnlineID<int>.OnlineID => OnlineID ?? -1;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IBeatmapInfo
|
||||
|
||||
[JsonIgnore]
|
||||
string IBeatmapInfo.DifficultyName => Version;
|
||||
|
||||
[JsonIgnore]
|
||||
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata ?? BeatmapSet?.Metadata ?? new BeatmapMetadata();
|
||||
|
||||
@ -197,9 +195,6 @@ namespace osu.Game.Beatmaps
|
||||
[JsonIgnore]
|
||||
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
|
||||
|
||||
[JsonIgnore]
|
||||
double IBeatmapInfo.StarRating => StarDifficulty;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// A user-presentable display title representing this beatmap.
|
||||
/// </summary>
|
||||
public static string GetDisplayTitle(this IBeatmapInfo beatmapInfo) => $"{beatmapInfo.Metadata} {getVersionString(beatmapInfo)}".Trim();
|
||||
public static string GetDisplayTitle(this IBeatmapInfo beatmapInfo) => $"{beatmapInfo.Metadata.GetDisplayTitle()} {getVersionString(beatmapInfo)}".Trim();
|
||||
|
||||
/// <summary>
|
||||
/// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields.
|
||||
|
@ -10,7 +10,8 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Audio.Mixing;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
@ -31,18 +32,23 @@ namespace osu.Game.Beatmaps
|
||||
[ExcludeFromDynamicCompile]
|
||||
public class BeatmapManager : IModelDownloader<IBeatmapSetInfo>, IModelManager<BeatmapSetInfo>, IModelFileManager<BeatmapSetInfo, BeatmapSetFileInfo>, IModelImporter<BeatmapSetInfo>, IWorkingBeatmapCache, IDisposable
|
||||
{
|
||||
public ITrackStore BeatmapTrackStore { get; }
|
||||
|
||||
private readonly BeatmapModelManager beatmapModelManager;
|
||||
private readonly BeatmapModelDownloader beatmapModelDownloader;
|
||||
|
||||
private readonly WorkingBeatmapCache workingBeatmapCache;
|
||||
private readonly BeatmapOnlineLookupQueue onlineBeatmapLookupQueue;
|
||||
|
||||
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host = null,
|
||||
WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false)
|
||||
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> gameResources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false, AudioMixer mainTrackMixer = null)
|
||||
{
|
||||
var userResources = new FileStore(contextFactory, storage).Store;
|
||||
|
||||
BeatmapTrackStore = audioManager.GetTrackStore(userResources);
|
||||
|
||||
beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, api, host);
|
||||
beatmapModelDownloader = CreateBeatmapModelDownloader(beatmapModelManager, api, host);
|
||||
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, resources, new FileStore(contextFactory, storage).Store, defaultBeatmap, host);
|
||||
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
|
||||
|
||||
workingBeatmapCache.BeatmapManager = beatmapModelManager;
|
||||
beatmapModelManager.WorkingBeatmapCache = workingBeatmapCache;
|
||||
@ -59,8 +65,10 @@ namespace osu.Game.Beatmaps
|
||||
return new BeatmapModelDownloader(modelManager, api, host);
|
||||
}
|
||||
|
||||
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost host) =>
|
||||
new WorkingBeatmapCache(audioManager, resources, storage, defaultBeatmap, host);
|
||||
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost host)
|
||||
{
|
||||
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host);
|
||||
}
|
||||
|
||||
protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) =>
|
||||
new BeatmapModelManager(storage, contextFactory, rulesets, host);
|
||||
@ -101,12 +109,20 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// Fired when a single difficulty has been hidden.
|
||||
/// </summary>
|
||||
public IBindable<WeakReference<BeatmapInfo>> BeatmapHidden => beatmapModelManager.BeatmapHidden;
|
||||
public event Action<BeatmapInfo> BeatmapHidden
|
||||
{
|
||||
add => beatmapModelManager.BeatmapHidden += value;
|
||||
remove => beatmapModelManager.BeatmapHidden -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a single difficulty has been restored.
|
||||
/// </summary>
|
||||
public IBindable<WeakReference<BeatmapInfo>> BeatmapRestored => beatmapModelManager.BeatmapRestored;
|
||||
public event Action<BeatmapInfo> BeatmapRestored
|
||||
{
|
||||
add => beatmapModelManager.BeatmapRestored += value;
|
||||
remove => beatmapModelManager.BeatmapRestored -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
|
||||
@ -198,9 +214,17 @@ namespace osu.Game.Beatmaps
|
||||
return beatmapModelManager.IsAvailableLocally(model);
|
||||
}
|
||||
|
||||
public IBindable<WeakReference<BeatmapSetInfo>> ItemUpdated => beatmapModelManager.ItemUpdated;
|
||||
public event Action<BeatmapSetInfo> ItemUpdated
|
||||
{
|
||||
add => beatmapModelManager.ItemUpdated += value;
|
||||
remove => beatmapModelManager.ItemUpdated -= value;
|
||||
}
|
||||
|
||||
public IBindable<WeakReference<BeatmapSetInfo>> ItemRemoved => beatmapModelManager.ItemRemoved;
|
||||
public event Action<BeatmapSetInfo> ItemRemoved
|
||||
{
|
||||
add => beatmapModelManager.ItemRemoved += value;
|
||||
remove => beatmapModelManager.ItemRemoved -= value;
|
||||
}
|
||||
|
||||
public Task ImportFromStableAsync(StableStorage stableStorage)
|
||||
{
|
||||
@ -246,9 +270,17 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
#region Implementation of IModelDownloader<BeatmapSetInfo>
|
||||
|
||||
public IBindable<WeakReference<ArchiveDownloadRequest<IBeatmapSetInfo>>> DownloadBegan => beatmapModelDownloader.DownloadBegan;
|
||||
public event Action<ArchiveDownloadRequest<IBeatmapSetInfo>> DownloadBegan
|
||||
{
|
||||
add => beatmapModelDownloader.DownloadBegan += value;
|
||||
remove => beatmapModelDownloader.DownloadBegan -= value;
|
||||
}
|
||||
|
||||
public IBindable<WeakReference<ArchiveDownloadRequest<IBeatmapSetInfo>>> DownloadFailed => beatmapModelDownloader.DownloadFailed;
|
||||
public event Action<ArchiveDownloadRequest<IBeatmapSetInfo>> DownloadFailed
|
||||
{
|
||||
add => beatmapModelDownloader.DownloadFailed += value;
|
||||
remove => beatmapModelDownloader.DownloadFailed -= value;
|
||||
}
|
||||
|
||||
public bool Download(IBeatmapSetInfo model, bool minimiseDownloadSize = false) =>
|
||||
beatmapModelDownloader.Download(model, minimiseDownloadSize);
|
||||
|
@ -27,8 +27,12 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
public static string GetDisplayTitle(this IBeatmapMetadataInfo metadataInfo)
|
||||
{
|
||||
string author = string.IsNullOrEmpty(metadataInfo.Author.Username) ? string.Empty : $"({metadataInfo.Author})";
|
||||
return $"{metadataInfo.Artist} - {metadataInfo.Title} {author}".Trim();
|
||||
string author = string.IsNullOrEmpty(metadataInfo.Author.Username) ? string.Empty : $" ({metadataInfo.Author.Username})";
|
||||
|
||||
string artist = string.IsNullOrEmpty(metadataInfo.Artist) ? "unknown artist" : metadataInfo.Artist;
|
||||
string title = string.IsNullOrEmpty(metadataInfo.Title) ? "unknown title" : metadataInfo.Title;
|
||||
|
||||
return $"{artist} - {title}{author}".Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -11,7 +11,6 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Logging;
|
||||
@ -37,14 +36,12 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// Fired when a single difficulty has been hidden.
|
||||
/// </summary>
|
||||
public IBindable<WeakReference<BeatmapInfo>> BeatmapHidden => beatmapHidden;
|
||||
|
||||
private readonly Bindable<WeakReference<BeatmapInfo>> beatmapHidden = new Bindable<WeakReference<BeatmapInfo>>();
|
||||
public event Action<BeatmapInfo> BeatmapHidden;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a single difficulty has been restored.
|
||||
/// </summary>
|
||||
public IBindable<WeakReference<BeatmapInfo>> BeatmapRestored => beatmapRestored;
|
||||
public event Action<BeatmapInfo> BeatmapRestored;
|
||||
|
||||
/// <summary>
|
||||
/// An online lookup queue component which handles populating online beatmap metadata.
|
||||
@ -56,8 +53,6 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
public IWorkingBeatmapCache WorkingBeatmapCache { private get; set; }
|
||||
|
||||
private readonly Bindable<WeakReference<BeatmapInfo>> beatmapRestored = new Bindable<WeakReference<BeatmapInfo>>();
|
||||
|
||||
public override IEnumerable<string> HandledExtensions => new[] { ".osz" };
|
||||
|
||||
protected override string[] HashableFileTypes => new[] { ".osu" };
|
||||
@ -75,8 +70,8 @@ namespace osu.Game.Beatmaps
|
||||
this.rulesets = rulesets;
|
||||
|
||||
beatmaps = (BeatmapStore)ModelStore;
|
||||
beatmaps.BeatmapHidden += b => beatmapHidden.Value = new WeakReference<BeatmapInfo>(b);
|
||||
beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference<BeatmapInfo>(b);
|
||||
beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b);
|
||||
beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b);
|
||||
beatmaps.ItemRemoved += b => WorkingBeatmapCache?.Invalidate(b);
|
||||
beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj);
|
||||
}
|
||||
@ -99,17 +94,17 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
validateOnlineIds(beatmapSet);
|
||||
|
||||
bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0);
|
||||
bool hadOnlineIDs = beatmapSet.Beatmaps.Any(b => b.OnlineID > 0);
|
||||
|
||||
if (OnlineLookupQueue != null)
|
||||
await OnlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
|
||||
if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0))
|
||||
if (hadOnlineIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineID > 0))
|
||||
{
|
||||
if (beatmapSet.OnlineBeatmapSetID != null)
|
||||
if (beatmapSet.OnlineID != null)
|
||||
{
|
||||
beatmapSet.OnlineBeatmapSetID = null;
|
||||
beatmapSet.OnlineID = null;
|
||||
LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs");
|
||||
}
|
||||
}
|
||||
@ -121,27 +116,27 @@ namespace osu.Game.Beatmaps
|
||||
throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}.");
|
||||
|
||||
// check if a set already exists with the same online id, delete if it does.
|
||||
if (beatmapSet.OnlineBeatmapSetID != null)
|
||||
if (beatmapSet.OnlineID != null)
|
||||
{
|
||||
var existingSetWithSameOnlineID = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID);
|
||||
var existingSetWithSameOnlineID = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineID == beatmapSet.OnlineID);
|
||||
|
||||
if (existingSetWithSameOnlineID != null)
|
||||
{
|
||||
Delete(existingSetWithSameOnlineID);
|
||||
|
||||
// in order to avoid a unique key constraint, immediately remove the online ID from the previous set.
|
||||
existingSetWithSameOnlineID.OnlineBeatmapSetID = null;
|
||||
existingSetWithSameOnlineID.OnlineID = null;
|
||||
foreach (var b in existingSetWithSameOnlineID.Beatmaps)
|
||||
b.OnlineBeatmapID = null;
|
||||
b.OnlineID = null;
|
||||
|
||||
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted.");
|
||||
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineID}). It has been deleted.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateOnlineIds(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
|
||||
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineID.HasValue).Select(b => b.OnlineID).ToList();
|
||||
|
||||
// ensure all IDs are unique
|
||||
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
|
||||
@ -152,7 +147,7 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
|
||||
// find any existing beatmaps in the database that have matching online ids
|
||||
var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).ToList();
|
||||
var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineID)).ToList();
|
||||
|
||||
if (existingBeatmaps.Count > 0)
|
||||
{
|
||||
@ -167,7 +162,7 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null);
|
||||
void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineID = null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -194,7 +189,11 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
// Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
|
||||
// This should hopefully be temporary, assuming said clone is eventually removed.
|
||||
beatmapInfo.BaseDifficulty.CopyFrom(beatmapContent.Difficulty);
|
||||
|
||||
// Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)
|
||||
// *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).
|
||||
// CopyTo() will undo such adjustments, while CopyFrom() will not.
|
||||
beatmapContent.Difficulty.CopyTo(beatmapInfo.BaseDifficulty);
|
||||
|
||||
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
|
||||
beatmapContent.BeatmapInfo = beatmapInfo;
|
||||
@ -216,7 +215,7 @@ namespace osu.Game.Beatmaps
|
||||
var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo();
|
||||
|
||||
// metadata may have changed; update the path with the standard format.
|
||||
beatmapInfo.Path = GetValidFilename($"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}].osu");
|
||||
beatmapInfo.Path = GetValidFilename($"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu");
|
||||
|
||||
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
|
||||
|
||||
@ -243,7 +242,7 @@ namespace osu.Game.Beatmaps
|
||||
if (!base.CanSkipImport(existing, import))
|
||||
return false;
|
||||
|
||||
return existing.Beatmaps.Any(b => b.OnlineBeatmapID != null);
|
||||
return existing.Beatmaps.Any(b => b.OnlineID != null);
|
||||
}
|
||||
|
||||
protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import)
|
||||
@ -251,11 +250,11 @@ namespace osu.Game.Beatmaps
|
||||
if (!base.CanReuseExisting(existing, import))
|
||||
return false;
|
||||
|
||||
var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
|
||||
var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
|
||||
var existingIds = existing.Beatmaps.Select(b => b.OnlineID).OrderBy(i => i);
|
||||
var importIds = import.Beatmaps.Select(b => b.OnlineID).OrderBy(i => i);
|
||||
|
||||
// force re-import if we are not in a sane state.
|
||||
return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds);
|
||||
return existing.OnlineID == import.OnlineID && existingIds.SequenceEqual(importIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -350,7 +349,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable<BeatmapSetInfo> items)
|
||||
=> base.CheckLocalAvailability(model, items)
|
||||
|| (model.OnlineBeatmapSetID != null && items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID));
|
||||
|| (model.OnlineID != null && items.Any(b => b.OnlineID == model.OnlineID));
|
||||
|
||||
protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
|
||||
{
|
||||
@ -369,7 +368,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
return new BeatmapSetInfo
|
||||
{
|
||||
OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID,
|
||||
OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineID,
|
||||
Beatmaps = new List<BeatmapInfo>(),
|
||||
Metadata = beatmap.Metadata,
|
||||
DateAdded = DateTimeOffset.UtcNow
|
||||
@ -408,7 +407,7 @@ namespace osu.Game.Beatmaps
|
||||
beatmap.BeatmapInfo.Ruleset = ruleset;
|
||||
|
||||
// TODO: this should be done in a better place once we actually need to dynamically update it.
|
||||
beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0;
|
||||
beatmap.BeatmapInfo.StarRating = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0;
|
||||
beatmap.BeatmapInfo.Length = calculateLength(beatmap);
|
||||
beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength();
|
||||
|
||||
|
@ -84,8 +84,8 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
beatmapInfo.Status = res.Status;
|
||||
beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapSetOnlineStatus.None;
|
||||
beatmapInfo.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
|
||||
beatmapInfo.OnlineBeatmapID = res.OnlineID;
|
||||
beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID;
|
||||
beatmapInfo.OnlineID = res.OnlineID;
|
||||
|
||||
if (beatmapInfo.Metadata != null)
|
||||
beatmapInfo.Metadata.AuthorID = res.AuthorID;
|
||||
@ -103,7 +103,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
void fail(Exception e)
|
||||
{
|
||||
beatmapInfo.OnlineBeatmapID = null;
|
||||
beatmapInfo.OnlineID = null;
|
||||
logForModel(set, $"Online retrieval failed for {beatmapInfo} ({e.Message})");
|
||||
}
|
||||
}
|
||||
@ -161,7 +161,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
if (string.IsNullOrEmpty(beatmapInfo.MD5Hash)
|
||||
&& string.IsNullOrEmpty(beatmapInfo.Path)
|
||||
&& beatmapInfo.OnlineBeatmapID == null)
|
||||
&& beatmapInfo.OnlineID == null)
|
||||
return false;
|
||||
|
||||
try
|
||||
@ -172,10 +172,10 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
using (var cmd = db.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path";
|
||||
cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path";
|
||||
|
||||
cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmapInfo.MD5Hash));
|
||||
cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmapInfo.OnlineBeatmapID ?? (object)DBNull.Value));
|
||||
cmd.Parameters.Add(new SqliteParameter("@OnlineID", beatmapInfo.OnlineID ?? (object)DBNull.Value));
|
||||
cmd.Parameters.Add(new SqliteParameter("@Path", beatmapInfo.Path));
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
@ -186,8 +186,8 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
beatmapInfo.Status = status;
|
||||
beatmapInfo.BeatmapSet.Status = status;
|
||||
beatmapInfo.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0);
|
||||
beatmapInfo.OnlineBeatmapID = reader.GetInt32(1);
|
||||
beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0);
|
||||
beatmapInfo.OnlineID = reader.GetInt32(1);
|
||||
|
||||
if (beatmapInfo.Metadata != null)
|
||||
beatmapInfo.Metadata.AuthorID = reader.GetInt32(3);
|
||||
|
25
osu.Game/Beatmaps/BeatmapSetHypeStatus.cs
Normal file
25
osu.Game/Beatmaps/BeatmapSetHypeStatus.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// 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 Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains information about the current hype status of a beatmap set.
|
||||
/// </summary>
|
||||
public class BeatmapSetHypeStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The current number of hypes that the set has received.
|
||||
/// </summary>
|
||||
[JsonProperty(@"current")]
|
||||
public int Current { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of hypes required so that the set is eligible for nomination.
|
||||
/// </summary>
|
||||
[JsonProperty(@"required")]
|
||||
public int Required { get; set; }
|
||||
}
|
||||
}
|
@ -16,12 +16,13 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
public int ID { get; set; }
|
||||
|
||||
private int? onlineBeatmapSetID;
|
||||
private int? onlineID;
|
||||
|
||||
public int? OnlineBeatmapSetID
|
||||
[Column("OnlineBeatmapSetID")]
|
||||
public int? OnlineID
|
||||
{
|
||||
get => onlineBeatmapSetID;
|
||||
set => onlineBeatmapSetID = value > 0 ? value : null;
|
||||
get => onlineID;
|
||||
set => onlineID = value > 0 ? value : null;
|
||||
}
|
||||
|
||||
public DateTimeOffset DateAdded { get; set; }
|
||||
@ -38,7 +39,7 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// The maximum star difficulty of all beatmaps in this set.
|
||||
/// </summary>
|
||||
public double MaxStarDifficulty => Beatmaps?.Max(b => b.StarDifficulty) ?? 0;
|
||||
public double MaxStarDifficulty => Beatmaps?.Max(b => b.StarRating) ?? 0;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum playable length in milliseconds of all beatmaps in this set.
|
||||
@ -74,8 +75,8 @@ namespace osu.Game.Beatmaps
|
||||
if (ID != 0 && other.ID != 0)
|
||||
return ID == other.ID;
|
||||
|
||||
if (OnlineBeatmapSetID.HasValue && other.OnlineBeatmapSetID.HasValue)
|
||||
return OnlineBeatmapSetID == other.OnlineBeatmapSetID;
|
||||
if (OnlineID.HasValue && other.OnlineID.HasValue)
|
||||
return OnlineID == other.OnlineID;
|
||||
|
||||
if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash))
|
||||
return Hash == other.Hash;
|
||||
@ -85,7 +86,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
#region Implementation of IHasOnlineID
|
||||
|
||||
public int OnlineID => OnlineBeatmapSetID ?? -1;
|
||||
int IHasOnlineID<int>.OnlineID => OnlineID ?? -1;
|
||||
|
||||
#endregion
|
||||
|
||||
|
25
osu.Game/Beatmaps/BeatmapSetNominationStatus.cs
Normal file
25
osu.Game/Beatmaps/BeatmapSetNominationStatus.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// 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 Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains information about the current nomination status of a beatmap set.
|
||||
/// </summary>
|
||||
public class BeatmapSetNominationStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The current number of nominations that the set has received.
|
||||
/// </summary>
|
||||
[JsonProperty(@"current")]
|
||||
public int Current { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of nominations required so that the map is eligible for qualification.
|
||||
/// </summary>
|
||||
[JsonProperty(@"required")]
|
||||
public int Required { get; set; }
|
||||
}
|
||||
}
|
@ -64,7 +64,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b =>
|
||||
{
|
||||
double difference = b.StarDifficulty - recommendation;
|
||||
double difference = b.StarRating - recommendation;
|
||||
return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder
|
||||
}).FirstOrDefault();
|
||||
|
||||
|
@ -1,13 +1,17 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps.Drawables.Cards.Buttons;
|
||||
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -19,6 +23,7 @@ using osuTK;
|
||||
using osu.Game.Overlays.BeatmapListing.Panels;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK.Graphics;
|
||||
using DownloadButton = osu.Game.Beatmaps.Drawables.Cards.Buttons.DownloadButton;
|
||||
|
||||
namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
{
|
||||
@ -31,15 +36,19 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
private const float corner_radius = 10;
|
||||
|
||||
private readonly APIBeatmapSet beatmapSet;
|
||||
private readonly Bindable<BeatmapSetFavouriteState> favouriteState;
|
||||
|
||||
private UpdateableOnlineBeatmapSetCover leftCover;
|
||||
private FillFlowContainer iconArea;
|
||||
private FillFlowContainer leftIconArea;
|
||||
|
||||
private Container rightButtonArea;
|
||||
|
||||
private Container mainContent;
|
||||
private BeatmapCardContentBackground mainContentBackground;
|
||||
|
||||
private GridContainer titleContainer;
|
||||
private GridContainer artistContainer;
|
||||
private FillFlowContainer<BeatmapCardStatistic> statisticsContainer;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; }
|
||||
@ -48,6 +57,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
: base(HoverSampleSet.Submit)
|
||||
{
|
||||
this.beatmapSet = beatmapSet;
|
||||
favouriteState = new Bindable<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount));
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -76,7 +86,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
OnlineInfo = beatmapSet
|
||||
},
|
||||
iconArea = new FillFlowContainer
|
||||
leftIconArea = new FillFlowContainer
|
||||
{
|
||||
Margin = new MarginPadding(5),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
@ -85,6 +95,27 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
}
|
||||
}
|
||||
},
|
||||
rightButtonArea = new Container
|
||||
{
|
||||
Name = @"Right (button) area",
|
||||
Width = 30,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Origin = Anchor.TopRight,
|
||||
Anchor = Anchor.TopRight,
|
||||
Child = new FillFlowContainer<BeatmapCardIconButton>
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 14),
|
||||
Children = new BeatmapCardIconButton[]
|
||||
{
|
||||
new FavouriteButton(beatmapSet) { Current = favouriteState },
|
||||
new DownloadButton(beatmapSet)
|
||||
}
|
||||
}
|
||||
},
|
||||
mainContent = new Container
|
||||
{
|
||||
Name = @"Main content",
|
||||
@ -176,6 +207,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
|
||||
d.AddUserLink(beatmapSet.Author);
|
||||
}),
|
||||
statisticsContainer = new FillFlowContainer<BeatmapCardStatistic>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(10, 0),
|
||||
Alpha = 0,
|
||||
ChildrenEnumerable = createStatistics()
|
||||
}
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
@ -214,10 +254,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
};
|
||||
|
||||
if (beatmapSet.HasVideo)
|
||||
iconArea.Add(new IconPill(FontAwesome.Solid.Film));
|
||||
leftIconArea.Add(new IconPill(FontAwesome.Solid.Film));
|
||||
|
||||
if (beatmapSet.HasStoryboard)
|
||||
iconArea.Add(new IconPill(FontAwesome.Solid.Image));
|
||||
leftIconArea.Add(new IconPill(FontAwesome.Solid.Image));
|
||||
|
||||
if (beatmapSet.HasExplicitContent)
|
||||
{
|
||||
@ -265,6 +305,24 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist);
|
||||
}
|
||||
|
||||
private IEnumerable<BeatmapCardStatistic> createStatistics()
|
||||
{
|
||||
if (beatmapSet.HypeStatus != null)
|
||||
yield return new HypesStatistic(beatmapSet.HypeStatus);
|
||||
|
||||
// web does not show nominations unless hypes are also present.
|
||||
// see: https://github.com/ppy/osu-web/blob/8ed7d071fd1d3eaa7e43cf0e4ff55ca2fef9c07c/resources/assets/lib/beatmapset-panel.tsx#L443
|
||||
if (beatmapSet.HypeStatus != null && beatmapSet.NominationStatus != null)
|
||||
yield return new NominationsStatistic(beatmapSet.NominationStatus);
|
||||
|
||||
yield return new FavouritesStatistic(beatmapSet) { Current = favouriteState };
|
||||
yield return new PlayCountStatistic(beatmapSet);
|
||||
|
||||
var dateStatistic = BeatmapCardDateStatistic.CreateFor(beatmapSet);
|
||||
if (dateStatistic != null)
|
||||
yield return dateStatistic;
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
float targetWidth = width - height;
|
||||
@ -275,6 +333,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
mainContentBackground.Dimmed.Value = IsHovered;
|
||||
|
||||
leftCover.FadeColour(IsHovered ? OsuColour.Gray(0.2f) : Color4.White, TRANSITION_DURATION, Easing.OutQuint);
|
||||
statisticsContainer.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
|
||||
rightButtonArea.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Beatmaps.Drawables.Cards.Buttons;
|
||||
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
|
||||
|
||||
namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores the current favourite state of a beatmap set.
|
||||
/// Used to coordinate between <see cref="FavouriteButton"/> and <see cref="FavouritesStatistic"/>.
|
||||
/// </summary>
|
||||
public readonly struct BeatmapSetFavouriteState
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the currently logged-in user has favourited this beatmap.
|
||||
/// </summary>
|
||||
public bool Favourited { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of favourites that the beatmap set has received, including the currently logged-in user.
|
||||
/// </summary>
|
||||
public int FavouriteCount { get; }
|
||||
|
||||
public BeatmapSetFavouriteState(bool favourited, int favouriteCount)
|
||||
{
|
||||
Favourited = favourited;
|
||||
FavouriteCount = favouriteCount;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
|
||||
{
|
||||
public abstract class BeatmapCardIconButton : OsuHoverContainer
|
||||
{
|
||||
protected readonly SpriteIcon Icon;
|
||||
|
||||
private float size;
|
||||
|
||||
public new float Size
|
||||
{
|
||||
get => size;
|
||||
set
|
||||
{
|
||||
size = value;
|
||||
Icon.Size = new Vector2(size);
|
||||
}
|
||||
}
|
||||
|
||||
protected BeatmapCardIconButton()
|
||||
{
|
||||
Add(Icon = new SpriteIcon());
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Size = 12;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
Anchor = Origin = Anchor.Centre;
|
||||
|
||||
IdleColour = colourProvider.Light1;
|
||||
HoverColour = colourProvider.Content1;
|
||||
}
|
||||
}
|
||||
}
|
18
osu.Game/Beatmaps/Drawables/Cards/Buttons/DownloadButton.cs
Normal file
18
osu.Game/Beatmaps/Drawables/Cards/Buttons/DownloadButton.cs
Normal file
@ -0,0 +1,18 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
|
||||
{
|
||||
public class DownloadButton : BeatmapCardIconButton
|
||||
{
|
||||
public DownloadButton(APIBeatmapSet beatmapSet)
|
||||
{
|
||||
Icon.Icon = FontAwesome.Solid.FileDownload;
|
||||
}
|
||||
|
||||
// TODO: implement behaviour
|
||||
}
|
||||
}
|
86
osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs
Normal file
86
osu.Game/Beatmaps/Drawables/Cards/Buttons/FavouriteButton.cs
Normal file
@ -0,0 +1,86 @@
|
||||
// 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.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
|
||||
{
|
||||
public class FavouriteButton : BeatmapCardIconButton, IHasCurrentValue<BeatmapSetFavouriteState>
|
||||
{
|
||||
private readonly BindableWithCurrent<BeatmapSetFavouriteState> current;
|
||||
|
||||
public Bindable<BeatmapSetFavouriteState> Current
|
||||
{
|
||||
get => current.Current;
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
private readonly APIBeatmapSet beatmapSet;
|
||||
|
||||
private PostBeatmapFavouriteRequest favouriteRequest;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
public FavouriteButton(APIBeatmapSet beatmapSet)
|
||||
{
|
||||
current = new BindableWithCurrent<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount));
|
||||
this.beatmapSet = beatmapSet;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Action = toggleFavouriteStatus;
|
||||
current.BindValueChanged(_ => updateState(), true);
|
||||
}
|
||||
|
||||
private void toggleFavouriteStatus()
|
||||
{
|
||||
var actionType = current.Value.Favourited ? BeatmapFavouriteAction.UnFavourite : BeatmapFavouriteAction.Favourite;
|
||||
|
||||
favouriteRequest?.Cancel();
|
||||
favouriteRequest = new PostBeatmapFavouriteRequest(beatmapSet.OnlineID, actionType);
|
||||
|
||||
Enabled.Value = false;
|
||||
favouriteRequest.Success += () =>
|
||||
{
|
||||
bool favourited = actionType == BeatmapFavouriteAction.Favourite;
|
||||
|
||||
current.Value = new BeatmapSetFavouriteState(favourited, current.Value.FavouriteCount + (favourited ? 1 : -1));
|
||||
|
||||
Enabled.Value = true;
|
||||
};
|
||||
favouriteRequest.Failure += e =>
|
||||
{
|
||||
Logger.Error(e, $"Failed to {actionType.ToString().ToLower()} beatmap: {e.Message}");
|
||||
Enabled.Value = true;
|
||||
};
|
||||
|
||||
api.Queue(favouriteRequest);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
if (current.Value.Favourited)
|
||||
{
|
||||
Icon.Icon = FontAwesome.Solid.Heart;
|
||||
TooltipText = BeatmapsetsStrings.ShowDetailsUnfavourite;
|
||||
}
|
||||
else
|
||||
{
|
||||
Icon.Icon = FontAwesome.Regular.Heart;
|
||||
TooltipText = BeatmapsetsStrings.ShowDetailsFavourite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
|
||||
{
|
||||
public class BeatmapCardDateStatistic : BeatmapCardStatistic
|
||||
{
|
||||
private readonly DateTimeOffset dateTime;
|
||||
|
||||
private BeatmapCardDateStatistic(DateTimeOffset dateTime)
|
||||
{
|
||||
this.dateTime = dateTime;
|
||||
|
||||
Icon = FontAwesome.Regular.CheckCircle;
|
||||
Text = dateTime.ToLocalisableString(@"d MMM yyyy");
|
||||
}
|
||||
|
||||
public override object TooltipContent => dateTime;
|
||||
public override ITooltip GetCustomTooltip() => new DateTooltip();
|
||||
|
||||
public static BeatmapCardDateStatistic? CreateFor(IBeatmapSetOnlineInfo beatmapSetInfo)
|
||||
{
|
||||
var displayDate = displayDateFor(beatmapSetInfo);
|
||||
|
||||
if (displayDate == null)
|
||||
return null;
|
||||
|
||||
return new BeatmapCardDateStatistic(displayDate.Value);
|
||||
}
|
||||
|
||||
private static DateTimeOffset? displayDateFor(IBeatmapSetOnlineInfo beatmapSetInfo)
|
||||
{
|
||||
// reference: https://github.com/ppy/osu-web/blob/ef432c11719fd1207bec5f9194b04f0033bdf02c/resources/assets/lib/beatmapset-panel.tsx#L36-L44
|
||||
switch (beatmapSetInfo.Status)
|
||||
{
|
||||
case BeatmapSetOnlineStatus.Ranked:
|
||||
case BeatmapSetOnlineStatus.Approved:
|
||||
case BeatmapSetOnlineStatus.Loved:
|
||||
case BeatmapSetOnlineStatus.Qualified:
|
||||
return beatmapSetInfo.Ranked;
|
||||
|
||||
default:
|
||||
return beatmapSetInfo.LastUpdated;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
|
||||
{
|
||||
/// <summary>
|
||||
/// A single statistic shown on a beatmap card.
|
||||
/// </summary>
|
||||
public abstract class BeatmapCardStatistic : CompositeDrawable, IHasTooltip, IHasCustomTooltip
|
||||
{
|
||||
protected IconUsage Icon
|
||||
{
|
||||
get => spriteIcon.Icon;
|
||||
set => spriteIcon.Icon = value;
|
||||
}
|
||||
|
||||
protected LocalisableString Text
|
||||
{
|
||||
get => spriteText.Text;
|
||||
set => spriteText.Text = value;
|
||||
}
|
||||
|
||||
public LocalisableString TooltipText { get; protected set; }
|
||||
|
||||
private readonly SpriteIcon spriteIcon;
|
||||
private readonly OsuSpriteText spriteText;
|
||||
|
||||
protected BeatmapCardStatistic()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(5, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
spriteIcon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Size = new Vector2(10),
|
||||
Margin = new MarginPadding { Top = 1 }
|
||||
},
|
||||
spriteText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.Default.With(size: 14)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
spriteIcon.Colour = colourProvider.Content2;
|
||||
}
|
||||
|
||||
#region Tooltip implementation
|
||||
|
||||
public virtual ITooltip GetCustomTooltip() => null;
|
||||
public virtual object TooltipContent => null;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Humanizer;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows the number of favourites that a beatmap set has received.
|
||||
/// </summary>
|
||||
public class FavouritesStatistic : BeatmapCardStatistic, IHasCurrentValue<BeatmapSetFavouriteState>
|
||||
{
|
||||
private readonly BindableWithCurrent<BeatmapSetFavouriteState> current;
|
||||
|
||||
public Bindable<BeatmapSetFavouriteState> Current
|
||||
{
|
||||
get => current.Current;
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
public FavouritesStatistic(IBeatmapSetOnlineInfo onlineInfo)
|
||||
{
|
||||
current = new BindableWithCurrent<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(onlineInfo.HasFavourited, onlineInfo.FavouriteCount));
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
current.BindValueChanged(_ => updateState(), true);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
Icon = current.Value.Favourited ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart;
|
||||
Text = current.Value.FavouriteCount.ToMetric(decimals: 1);
|
||||
TooltipText = BeatmapsStrings.PanelFavourites(current.Value.FavouriteCount.ToLocalisableString(@"N0"));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
// 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.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows the number of current hypes that a map has received, as well as the number of hypes required for nomination.
|
||||
/// </summary>
|
||||
public class HypesStatistic : BeatmapCardStatistic
|
||||
{
|
||||
public HypesStatistic(BeatmapSetHypeStatus hypeStatus)
|
||||
{
|
||||
Icon = FontAwesome.Solid.Bullhorn;
|
||||
Text = hypeStatus.Current.ToLocalisableString();
|
||||
TooltipText = BeatmapsStrings.HypeRequiredText(hypeStatus.Current.ToLocalisableString(), hypeStatus.Required.ToLocalisableString());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
// 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.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows the number of current nominations that a map has received, as well as the number of nominations required for qualification.
|
||||
/// </summary>
|
||||
public class NominationsStatistic : BeatmapCardStatistic
|
||||
{
|
||||
public NominationsStatistic(BeatmapSetNominationStatus nominationStatus)
|
||||
{
|
||||
Icon = FontAwesome.Solid.ThumbsUp;
|
||||
Text = nominationStatus.Current.ToLocalisableString();
|
||||
TooltipText = BeatmapsStrings.NominationsRequiredText(nominationStatus.Current.ToLocalisableString(), nominationStatus.Required.ToLocalisableString());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
// 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 Humanizer;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows the number of times the given beatmap set has been played.
|
||||
/// </summary>
|
||||
public class PlayCountStatistic : BeatmapCardStatistic
|
||||
{
|
||||
public PlayCountStatistic(IBeatmapSetOnlineInfo onlineInfo)
|
||||
{
|
||||
Icon = FontAwesome.Regular.PlayCircle;
|
||||
Text = onlineInfo.PlayCount.ToMetric(decimals: 1);
|
||||
TooltipText = BeatmapsStrings.PanelPlaycount(onlineInfo.PlayCount.ToLocalisableString(@"N0"));
|
||||
}
|
||||
}
|
||||
}
|
@ -251,7 +251,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
break;
|
||||
|
||||
case @"Version":
|
||||
beatmap.BeatmapInfo.Version = pair.Value;
|
||||
beatmap.BeatmapInfo.DifficultyName = pair.Value;
|
||||
break;
|
||||
|
||||
case @"Source":
|
||||
@ -263,11 +263,11 @@ namespace osu.Game.Beatmaps.Formats
|
||||
break;
|
||||
|
||||
case @"BeatmapID":
|
||||
beatmap.BeatmapInfo.OnlineBeatmapID = Parsing.ParseInt(pair.Value);
|
||||
beatmap.BeatmapInfo.OnlineID = Parsing.ParseInt(pair.Value);
|
||||
break;
|
||||
|
||||
case @"BeatmapSetID":
|
||||
beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = Parsing.ParseInt(pair.Value) };
|
||||
beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo { OnlineID = Parsing.ParseInt(pair.Value) };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -130,11 +130,11 @@ namespace osu.Game.Beatmaps.Formats
|
||||
writer.WriteLine(FormattableString.Invariant($"Artist: {beatmap.Metadata.Artist}"));
|
||||
if (!string.IsNullOrEmpty(beatmap.Metadata.ArtistUnicode)) writer.WriteLine(FormattableString.Invariant($"ArtistUnicode: {beatmap.Metadata.ArtistUnicode}"));
|
||||
writer.WriteLine(FormattableString.Invariant($"Creator: {beatmap.Metadata.Author.Username}"));
|
||||
writer.WriteLine(FormattableString.Invariant($"Version: {beatmap.BeatmapInfo.Version}"));
|
||||
writer.WriteLine(FormattableString.Invariant($"Version: {beatmap.BeatmapInfo.DifficultyName}"));
|
||||
if (!string.IsNullOrEmpty(beatmap.Metadata.Source)) writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}"));
|
||||
if (!string.IsNullOrEmpty(beatmap.Metadata.Tags)) writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}"));
|
||||
if (beatmap.BeatmapInfo.OnlineBeatmapID != null) writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineBeatmapID}"));
|
||||
if (beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID != null) writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID}"));
|
||||
if (beatmap.BeatmapInfo.OnlineID != null) writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineID}"));
|
||||
if (beatmap.BeatmapInfo.BeatmapSet?.OnlineID != null) writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineID}"));
|
||||
}
|
||||
|
||||
private void handleDifficulty(TextWriter writer)
|
||||
|
@ -102,5 +102,19 @@ namespace osu.Game.Beatmaps
|
||||
/// Total vote counts of user ratings on a scale of 0..10 where 0 is unused (probably will be fixed at API?).
|
||||
/// </summary>
|
||||
int[]? Ratings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Contains the current hype status of the beatmap set.
|
||||
/// Non-null only for <see cref="BeatmapSetOnlineStatus.WIP"/>, <see cref="BeatmapSetOnlineStatus.Pending"/>, and <see cref="BeatmapSetOnlineStatus.Qualified"/> sets.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See: https://github.com/ppy/osu-web/blob/93930cd02cfbd49724929912597c727c9fbadcd1/app/Models/Beatmapset.php#L155
|
||||
/// </remarks>
|
||||
BeatmapSetHypeStatus? HypeStatus { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Contains the current nomination status of the beatmap set.
|
||||
/// </summary>
|
||||
BeatmapSetNominationStatus? NominationStatus { get; }
|
||||
}
|
||||
}
|
||||
|
@ -17,33 +17,69 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
public interface IWorkingBeatmap
|
||||
{
|
||||
IBeatmapInfo BeatmapInfo { get; }
|
||||
|
||||
IBeatmapSetInfo BeatmapSetInfo { get; }
|
||||
|
||||
IBeatmapMetadataInfo Metadata { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="IBeatmap"/> which this <see cref="WorkingBeatmap"/> represents.
|
||||
/// Whether the Beatmap has finished loading.
|
||||
///</summary>
|
||||
public bool BeatmapLoaded { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Background has finished loading.
|
||||
///</summary>
|
||||
public bool BackgroundLoaded { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Waveform has finished loading.
|
||||
///</summary>
|
||||
public bool WaveformLoaded { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Storyboard has finished loading.
|
||||
///</summary>
|
||||
public bool StoryboardLoaded { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Skin has finished loading.
|
||||
///</summary>
|
||||
public bool SkinLoaded { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Track has finished loading.
|
||||
///</summary>
|
||||
public bool TrackLoaded { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="IBeatmap"/> which this <see cref="IWorkingBeatmap"/> represents.
|
||||
/// </summary>
|
||||
IBeatmap Beatmap { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the background for this <see cref="WorkingBeatmap"/>.
|
||||
/// Retrieves the background for this <see cref="IWorkingBeatmap"/>.
|
||||
/// </summary>
|
||||
Texture Background { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="Waveform"/> for the <see cref="Track"/> of this <see cref="WorkingBeatmap"/>.
|
||||
/// Retrieves the <see cref="Waveform"/> for the <see cref="Track"/> of this <see cref="IWorkingBeatmap"/>.
|
||||
/// </summary>
|
||||
Waveform Waveform { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="Storyboard"/> which this <see cref="WorkingBeatmap"/> provides.
|
||||
/// Retrieves the <see cref="Storyboard"/> which this <see cref="IWorkingBeatmap"/> provides.
|
||||
/// </summary>
|
||||
Storyboard Storyboard { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="Skin"/> which this <see cref="WorkingBeatmap"/> provides.
|
||||
/// Retrieves the <see cref="Skin"/> which this <see cref="IWorkingBeatmap"/> provides.
|
||||
/// </summary>
|
||||
ISkin Skin { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="Track"/> which this <see cref="WorkingBeatmap"/> has loaded.
|
||||
/// Retrieves the <see cref="Track"/> which this <see cref="IWorkingBeatmap"/> has loaded.
|
||||
/// </summary>
|
||||
Track Track { get; }
|
||||
|
||||
@ -67,7 +103,7 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In a standard game context, the loading of the track is managed solely by MusicController, which will
|
||||
/// automatically load the track of the current global IBindable WorkingBeatmap.
|
||||
/// automatically load the track of the current global IBindable IWorkingBeatmap.
|
||||
/// As such, this method should only be called in very special scenarios, such as external tests or apps which are
|
||||
/// outside of the game context.
|
||||
/// </remarks>
|
||||
@ -79,5 +115,20 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
/// <param name="storagePath">The storage path to the file.</param>
|
||||
Stream GetStream(string storagePath);
|
||||
|
||||
/// <summary>
|
||||
/// Beings loading the contents of this <see cref="IWorkingBeatmap"/> asynchronously.
|
||||
/// </summary>
|
||||
public void BeginAsyncLoad();
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the asynchronous loading of the contents of this <see cref="IWorkingBeatmap"/>.
|
||||
/// </summary>
|
||||
public void CancelAsyncLoad();
|
||||
|
||||
/// <summary>
|
||||
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
|
||||
/// </summary>
|
||||
void PrepareTrackForPreviewLooping();
|
||||
}
|
||||
}
|
||||
|
@ -27,9 +27,7 @@ namespace osu.Game.Beatmaps
|
||||
public abstract class WorkingBeatmap : IWorkingBeatmap
|
||||
{
|
||||
public readonly BeatmapInfo BeatmapInfo;
|
||||
|
||||
public readonly BeatmapSetInfo BeatmapSetInfo;
|
||||
|
||||
public readonly BeatmapMetadata Metadata;
|
||||
|
||||
protected AudioManager AudioManager { get; }
|
||||
@ -89,6 +87,9 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
IBeatmapConverter converter = CreateBeatmapConverter(Beatmap, rulesetInstance);
|
||||
|
||||
if (rulesetInstance == null)
|
||||
throw new RulesetLoadException("Creating ruleset instance failed when attempting to create playable beatmap.");
|
||||
|
||||
// Check if the beatmap can be converted
|
||||
if (Beatmap.HitObjects.Count > 0 && !converter.CanConvert())
|
||||
throw new BeatmapInvalidForRulesetException($"{nameof(Beatmaps.Beatmap)} can not be converted for the ruleset (ruleset: {ruleset.InstantiationInfo}, converter: {converter}).");
|
||||
@ -173,17 +174,8 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
private CancellationTokenSource loadCancellation = new CancellationTokenSource();
|
||||
|
||||
/// <summary>
|
||||
/// Beings loading the contents of this <see cref="WorkingBeatmap"/> asynchronously.
|
||||
/// </summary>
|
||||
public void BeginAsyncLoad()
|
||||
{
|
||||
loadBeatmapAsync();
|
||||
}
|
||||
public void BeginAsyncLoad() => loadBeatmapAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the asynchronous loading of the contents of this <see cref="WorkingBeatmap"/>.
|
||||
/// </summary>
|
||||
public void CancelAsyncLoad()
|
||||
{
|
||||
lock (beatmapFetchLock)
|
||||
@ -231,6 +223,10 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
|
||||
|
||||
IBeatmapInfo IWorkingBeatmap.BeatmapInfo => BeatmapInfo;
|
||||
IBeatmapMetadataInfo IWorkingBeatmap.Metadata => Metadata;
|
||||
IBeatmapSetInfo IWorkingBeatmap.BeatmapSetInfo => BeatmapSetInfo;
|
||||
|
||||
public IBeatmap Beatmap
|
||||
{
|
||||
get
|
||||
@ -270,9 +266,6 @@ namespace osu.Game.Beatmaps
|
||||
[NotNull]
|
||||
public Track LoadTrack() => loadedTrack = GetBeatmapTrack() ?? GetVirtualTrack(1000);
|
||||
|
||||
/// <summary>
|
||||
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
|
||||
/// </summary>
|
||||
public void PrepareTrackForPreviewLooping()
|
||||
{
|
||||
Track.Looping = true;
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Beatmaps
|
||||
[CanBeNull]
|
||||
private readonly GameHost host;
|
||||
|
||||
public WorkingBeatmapCache([NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> files, WorkingBeatmap defaultBeatmap = null, GameHost host = null)
|
||||
public WorkingBeatmapCache(ITrackStore trackStore, AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> files, WorkingBeatmap defaultBeatmap = null, GameHost host = null)
|
||||
{
|
||||
DefaultBeatmap = defaultBeatmap;
|
||||
|
||||
@ -50,7 +50,7 @@ namespace osu.Game.Beatmaps
|
||||
this.host = host;
|
||||
this.files = files;
|
||||
largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(files));
|
||||
trackStore = audioManager.GetTrackStore(files);
|
||||
this.trackStore = trackStore;
|
||||
}
|
||||
|
||||
public void Invalidate(BeatmapSetInfo info)
|
||||
|
@ -10,12 +10,12 @@ using System.Threading.Tasks;
|
||||
using Humanizer;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.IPC;
|
||||
@ -63,17 +63,13 @@ namespace osu.Game.Database
|
||||
/// Fired when a new or updated <typeparamref name="TModel"/> becomes available in the database.
|
||||
/// This is not guaranteed to run on the update thread.
|
||||
/// </summary>
|
||||
public IBindable<WeakReference<TModel>> ItemUpdated => itemUpdated;
|
||||
|
||||
private readonly Bindable<WeakReference<TModel>> itemUpdated = new Bindable<WeakReference<TModel>>();
|
||||
public event Action<TModel> ItemUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a <typeparamref name="TModel"/> is removed from the database.
|
||||
/// This is not guaranteed to run on the update thread.
|
||||
/// </summary>
|
||||
public IBindable<WeakReference<TModel>> ItemRemoved => itemRemoved;
|
||||
|
||||
private readonly Bindable<WeakReference<TModel>> itemRemoved = new Bindable<WeakReference<TModel>>();
|
||||
public event Action<TModel> ItemRemoved;
|
||||
|
||||
public virtual IEnumerable<string> HandledExtensions => new[] { @".zip" };
|
||||
|
||||
@ -93,8 +89,8 @@ namespace osu.Game.Database
|
||||
ContextFactory = contextFactory;
|
||||
|
||||
ModelStore = modelStore;
|
||||
ModelStore.ItemUpdated += item => handleEvent(() => itemUpdated.Value = new WeakReference<TModel>(item));
|
||||
ModelStore.ItemRemoved += item => handleEvent(() => itemRemoved.Value = new WeakReference<TModel>(item));
|
||||
ModelStore.ItemUpdated += item => handleEvent(() => ItemUpdated?.Invoke(item));
|
||||
ModelStore.ItemRemoved += item => handleEvent(() => ItemRemoved?.Invoke(item));
|
||||
|
||||
exportStorage = storage.GetStorageForDirectory(@"exports");
|
||||
|
||||
@ -197,7 +193,7 @@ namespace osu.Game.Database
|
||||
else
|
||||
{
|
||||
notification.CompletionText = imported.Count == 1
|
||||
? $"Imported {imported.First().Value}!"
|
||||
? $"Imported {imported.First().Value.GetDisplayString()}!"
|
||||
: $"Imported {imported.Count} {HumanisedModelName}s!";
|
||||
|
||||
if (imported.Count > 0 && PostImport != null)
|
||||
@ -268,7 +264,7 @@ namespace osu.Game.Database
|
||||
model = CreateModel(archive);
|
||||
|
||||
if (model == null)
|
||||
return Task.FromResult<ILive<TModel>>(new EntityFrameworkLive<TModel>(null));
|
||||
return Task.FromResult<ILive<TModel>>(null);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Framework.Bindables;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
@ -18,13 +17,13 @@ namespace osu.Game.Database
|
||||
/// Fired when a <typeparamref name="T"/> download begins.
|
||||
/// This is NOT run on the update thread and should be scheduled.
|
||||
/// </summary>
|
||||
IBindable<WeakReference<ArchiveDownloadRequest<T>>> DownloadBegan { get; }
|
||||
event Action<ArchiveDownloadRequest<T>> DownloadBegan;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a <typeparamref name="T"/> download is interrupted, either due to user cancellation or failure.
|
||||
/// This is NOT run on the update thread and should be scheduled.
|
||||
/// </summary>
|
||||
IBindable<WeakReference<ArchiveDownloadRequest<T>>> DownloadFailed { get; }
|
||||
event Action<ArchiveDownloadRequest<T>> DownloadFailed;
|
||||
|
||||
/// <summary>
|
||||
/// Begin a download for the requested <typeparamref name="T"/>.
|
||||
|
@ -7,6 +7,8 @@ using System.Threading.Tasks;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
@ -26,7 +28,7 @@ namespace osu.Game.Database
|
||||
/// <param name="lowPriority">Whether this is a low priority import.</param>
|
||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||
/// <returns>The imported model, if successful.</returns>
|
||||
Task<ILive<TModel>> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default);
|
||||
Task<ILive<TModel>?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Silently import an item from an <see cref="ArchiveReader"/>.
|
||||
@ -34,7 +36,7 @@ namespace osu.Game.Database
|
||||
/// <param name="archive">The archive to be imported.</param>
|
||||
/// <param name="lowPriority">Whether this is a low priority import.</param>
|
||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||
Task<ILive<TModel>> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default);
|
||||
Task<ILive<TModel>?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Silently import an item from a <typeparamref name="TModel"/>.
|
||||
@ -43,7 +45,7 @@ namespace osu.Game.Database
|
||||
/// <param name="archive">An optional archive to use for model population.</param>
|
||||
/// <param name="lowPriority">Whether this is a low priority import.</param>
|
||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||
Task<ILive<TModel>> Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default);
|
||||
Task<ILive<TModel>?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// A user displayable name for the model type associated with this manager.
|
||||
|
@ -5,7 +5,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.IO;
|
||||
|
||||
namespace osu.Game.Database
|
||||
@ -18,16 +17,14 @@ namespace osu.Game.Database
|
||||
where TModel : class
|
||||
{
|
||||
/// <summary>
|
||||
/// A bindable which contains a weak reference to the last item that was updated.
|
||||
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
|
||||
/// Fired when an item is updated.
|
||||
/// </summary>
|
||||
IBindable<WeakReference<TModel>> ItemUpdated { get; }
|
||||
event Action<TModel> ItemUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// A bindable which contains a weak reference to the last item that was removed.
|
||||
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
|
||||
/// Fired when an item is removed.
|
||||
/// </summary>
|
||||
IBindable<WeakReference<TModel>> ItemRemoved { get; }
|
||||
event Action<TModel> ItemRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
|
||||
|
@ -47,10 +47,30 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
public ArchiveReader GetReader()
|
||||
{
|
||||
if (Stream != null)
|
||||
return new ZipArchiveReader(Stream, Path);
|
||||
return Stream != null
|
||||
? getReaderFrom(Stream)
|
||||
: getReaderFrom(Path);
|
||||
}
|
||||
|
||||
return getReaderFrom(Path);
|
||||
/// <summary>
|
||||
/// Creates an <see cref="ArchiveReader"/> from a stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">A seekable stream containing the archive content.</param>
|
||||
/// <returns>A reader giving access to the archive's content.</returns>
|
||||
private ArchiveReader getReaderFrom(Stream stream)
|
||||
{
|
||||
if (!(stream is MemoryStream memoryStream))
|
||||
{
|
||||
// This isn't used in any current path. May need to reconsider for performance reasons (ie. if we don't expect the incoming stream to be copied out).
|
||||
byte[] buffer = new byte[stream.Length];
|
||||
stream.Read(buffer, 0, (int)stream.Length);
|
||||
memoryStream = new MemoryStream(buffer);
|
||||
}
|
||||
|
||||
if (ZipUtils.IsZipArchive(memoryStream))
|
||||
return new ZipArchiveReader(memoryStream, Path);
|
||||
|
||||
return new LegacyByteArrayReader(memoryStream.ToArray(), Path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -6,9 +6,9 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Humanizer;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
|
||||
@ -20,13 +20,9 @@ namespace osu.Game.Database
|
||||
{
|
||||
public Action<Notification> PostNotification { protected get; set; }
|
||||
|
||||
public IBindable<WeakReference<ArchiveDownloadRequest<T>>> DownloadBegan => downloadBegan;
|
||||
public event Action<ArchiveDownloadRequest<T>> DownloadBegan;
|
||||
|
||||
private readonly Bindable<WeakReference<ArchiveDownloadRequest<T>>> downloadBegan = new Bindable<WeakReference<ArchiveDownloadRequest<T>>>();
|
||||
|
||||
public IBindable<WeakReference<ArchiveDownloadRequest<T>>> DownloadFailed => downloadFailed;
|
||||
|
||||
private readonly Bindable<WeakReference<ArchiveDownloadRequest<T>>> downloadFailed = new Bindable<WeakReference<ArchiveDownloadRequest<T>>>();
|
||||
public event Action<ArchiveDownloadRequest<T>> DownloadFailed;
|
||||
|
||||
private readonly IModelImporter<TModel> importer;
|
||||
private readonly IAPIProvider api;
|
||||
@ -55,7 +51,7 @@ namespace osu.Game.Database
|
||||
|
||||
DownloadNotification notification = new DownloadNotification
|
||||
{
|
||||
Text = $"Downloading {request.Model}",
|
||||
Text = $"Downloading {request.Model.GetDisplayString()}",
|
||||
};
|
||||
|
||||
request.DownloadProgressed += progress =>
|
||||
@ -73,7 +69,7 @@ namespace osu.Game.Database
|
||||
|
||||
// for now a failed import will be marked as a failed download for simplicity.
|
||||
if (!imported.Any())
|
||||
downloadFailed.Value = new WeakReference<ArchiveDownloadRequest<T>>(request);
|
||||
DownloadFailed?.Invoke(request);
|
||||
|
||||
CurrentDownloads.Remove(request);
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
@ -92,14 +88,14 @@ namespace osu.Game.Database
|
||||
|
||||
api.PerformAsync(request);
|
||||
|
||||
downloadBegan.Value = new WeakReference<ArchiveDownloadRequest<T>>(request);
|
||||
DownloadBegan?.Invoke(request);
|
||||
return true;
|
||||
|
||||
void triggerFailure(Exception error)
|
||||
{
|
||||
CurrentDownloads.Remove(request);
|
||||
|
||||
downloadFailed.Value = new WeakReference<ArchiveDownloadRequest<T>>(request);
|
||||
DownloadFailed?.Invoke(request);
|
||||
|
||||
notification.State = ProgressNotificationState.Cancelled;
|
||||
|
||||
|
@ -125,11 +125,11 @@ namespace osu.Game.Database
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<BeatmapInfo>().HasIndex(b => b.OnlineBeatmapID).IsUnique();
|
||||
modelBuilder.Entity<BeatmapInfo>().HasIndex(b => b.OnlineID).IsUnique();
|
||||
modelBuilder.Entity<BeatmapInfo>().HasIndex(b => b.MD5Hash);
|
||||
modelBuilder.Entity<BeatmapInfo>().HasIndex(b => b.Hash);
|
||||
|
||||
modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.OnlineBeatmapSetID).IsUnique();
|
||||
modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.OnlineID).IsUnique();
|
||||
modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.DeletePending);
|
||||
modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.Hash).IsUnique();
|
||||
|
||||
|
@ -209,7 +209,13 @@ namespace osu.Game.Database
|
||||
|
||||
case 9:
|
||||
// Pretty pointless to do this as beatmaps aren't really loaded via realm yet, but oh well.
|
||||
var oldMetadata = migration.OldRealm.DynamicApi.All(getMappedOrOriginalName(typeof(RealmBeatmapMetadata)));
|
||||
string metadataClassName = getMappedOrOriginalName(typeof(RealmBeatmapMetadata));
|
||||
|
||||
// May be coming from a version before `RealmBeatmapMetadata` existed.
|
||||
if (!migration.OldRealm.Schema.TryFindObjectSchema(metadataClassName, out _))
|
||||
return;
|
||||
|
||||
var oldMetadata = migration.OldRealm.DynamicApi.All(metadataClassName);
|
||||
var newMetadata = migration.NewRealm.All<RealmBeatmapMetadata>();
|
||||
|
||||
int metadataCount = newMetadata.Count();
|
||||
|
61
osu.Game/Extensions/ModelExtensions.cs
Normal file
61
osu.Game/Extensions/ModelExtensions.cs
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Extensions
|
||||
{
|
||||
public static class ModelExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a user-facing string representing the <paramref name="model"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Non-interface types without special handling will fall back to <see cref="object.ToString()"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Warning: This method is _purposefully_ not called <c>GetDisplayTitle()</c> like the others, because otherwise
|
||||
/// extension method type inference rules cause this method to call itself and cause a stack overflow.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static string GetDisplayString(this object model)
|
||||
{
|
||||
string result = null;
|
||||
|
||||
switch (model)
|
||||
{
|
||||
case IBeatmapSetInfo beatmapSetInfo:
|
||||
result = beatmapSetInfo.Metadata?.GetDisplayTitle();
|
||||
break;
|
||||
|
||||
case IBeatmapInfo beatmapInfo:
|
||||
result = beatmapInfo.GetDisplayTitle();
|
||||
break;
|
||||
|
||||
case IBeatmapMetadataInfo metadataInfo:
|
||||
result = metadataInfo.GetDisplayTitle();
|
||||
break;
|
||||
|
||||
case IScoreInfo scoreInfo:
|
||||
result = scoreInfo.GetDisplayTitle();
|
||||
break;
|
||||
|
||||
case IRulesetInfo rulesetInfo:
|
||||
result = rulesetInfo.Name;
|
||||
break;
|
||||
|
||||
case IUser user:
|
||||
result = user.Username;
|
||||
break;
|
||||
}
|
||||
|
||||
// fallback in case none of the above happens to match.
|
||||
result ??= model?.ToString() ?? @"null";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
@ -46,7 +46,7 @@ namespace osu.Game.Graphics.Containers
|
||||
AddText(text[previousLinkEnd..link.Index]);
|
||||
|
||||
string displayText = text.Substring(link.Index, link.Length);
|
||||
string linkArgument = link.Argument;
|
||||
object linkArgument = link.Argument;
|
||||
string tooltip = displayText == link.Url ? null : link.Url;
|
||||
|
||||
AddLink(displayText, link.Action, linkArgument, tooltip);
|
||||
@ -62,16 +62,16 @@ namespace osu.Game.Graphics.Containers
|
||||
public void AddLink(LocalisableString text, Action action, string tooltipText = null, Action<SpriteText> creationParameters = null)
|
||||
=> createLink(CreateChunkFor(text, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.Custom, string.Empty), tooltipText, action);
|
||||
|
||||
public void AddLink(LocalisableString text, LinkAction action, string argument, string tooltipText = null, Action<SpriteText> creationParameters = null)
|
||||
public void AddLink(LocalisableString text, LinkAction action, object argument, string tooltipText = null, Action<SpriteText> creationParameters = null)
|
||||
=> createLink(CreateChunkFor(text, true, CreateSpriteText, creationParameters), new LinkDetails(action, argument), tooltipText);
|
||||
|
||||
public void AddLink(IEnumerable<SpriteText> text, LinkAction action, string linkArgument, string tooltipText = null)
|
||||
public void AddLink(IEnumerable<SpriteText> text, LinkAction action, object linkArgument, string tooltipText = null)
|
||||
{
|
||||
createLink(new TextPartManual(text), new LinkDetails(action, linkArgument), tooltipText);
|
||||
}
|
||||
|
||||
public void AddUserLink(IUser user, Action<SpriteText> creationParameters = null)
|
||||
=> createLink(CreateChunkFor(user.Username, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user.OnlineID.ToString()), "view profile");
|
||||
=> createLink(CreateChunkFor(user.Username, true, CreateSpriteText, creationParameters), new LinkDetails(LinkAction.OpenUserProfile, user), "view profile");
|
||||
|
||||
private void createLink(ITextPart textPart, LinkDetails link, LocalisableString tooltipText, Action action = null)
|
||||
{
|
||||
@ -83,7 +83,7 @@ namespace osu.Game.Graphics.Containers
|
||||
game.HandleLink(link);
|
||||
// fallback to handle cases where OsuGame is not available, ie. tournament client.
|
||||
else if (link.Action == LinkAction.External)
|
||||
host.OpenUrlExternally(link.Argument);
|
||||
host.OpenUrlExternally(link.Argument.ToString());
|
||||
};
|
||||
|
||||
AddPart(new TextLink(textPart, tooltipText, onClickAction));
|
||||
|
@ -52,15 +52,18 @@ namespace osu.Game.Graphics
|
||||
|
||||
public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(new[]
|
||||
{
|
||||
(1.5f, Color4Extensions.FromHex("4fc0ff")),
|
||||
(0.1f, Color4Extensions.FromHex("aaaaaa")),
|
||||
(0.1f, Color4Extensions.FromHex("4290fb")),
|
||||
(1.25f, Color4Extensions.FromHex("4fc0ff")),
|
||||
(2.0f, Color4Extensions.FromHex("4fffd5")),
|
||||
(2.5f, Color4Extensions.FromHex("7cff4f")),
|
||||
(3.25f, Color4Extensions.FromHex("f6f05c")),
|
||||
(4.5f, Color4Extensions.FromHex("ff8068")),
|
||||
(6.0f, Color4Extensions.FromHex("ff3c71")),
|
||||
(7.0f, Color4Extensions.FromHex("6563de")),
|
||||
(8.0f, Color4Extensions.FromHex("18158e")),
|
||||
(8.0f, Color4.Black),
|
||||
(3.3f, Color4Extensions.FromHex("f6f05c")),
|
||||
(4.2f, Color4Extensions.FromHex("ff8068")),
|
||||
(4.9f, Color4Extensions.FromHex("ff4e6f")),
|
||||
(5.8f, Color4Extensions.FromHex("c645b8")),
|
||||
(6.7f, Color4Extensions.FromHex("6563de")),
|
||||
(7.7f, Color4Extensions.FromHex("18158e")),
|
||||
(9.0f, Color4.Black),
|
||||
}, (float)Math.Round(starDifficulty, 2, MidpointRounding.AwayFromZero));
|
||||
|
||||
/// <summary>
|
||||
|
@ -19,6 +19,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
@ -219,7 +220,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
decimal decimalPrecision = normalise(CurrentNumber.Precision.ToDecimal(NumberFormatInfo.InvariantInfo), max_decimal_digits);
|
||||
|
||||
// Find the number of significant digits (we could have less than 5 after normalize())
|
||||
int significantDigits = findPrecision(decimalPrecision);
|
||||
int significantDigits = FormatUtils.FindPrecision(decimalPrecision);
|
||||
|
||||
TooltipText = floatValue.ToString($"N{significantDigits}");
|
||||
}
|
||||
@ -248,23 +249,5 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// <returns>The normalised decimal.</returns>
|
||||
private decimal normalise(decimal d, int sd)
|
||||
=> decimal.Parse(Math.Round(d, sd).ToString(string.Concat("0.", new string('#', sd)), CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the number of digits after the decimal.
|
||||
/// </summary>
|
||||
/// <param name="d">The value to find the number of decimal digits for.</param>
|
||||
/// <returns>The number decimal digits.</returns>
|
||||
private int findPrecision(decimal d)
|
||||
{
|
||||
int precision = 0;
|
||||
|
||||
while (d != Math.Round(d))
|
||||
{
|
||||
d *= 10;
|
||||
precision++;
|
||||
}
|
||||
|
||||
return precision;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ using System.IO;
|
||||
namespace osu.Game.IO.Archives
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows reading a single file from the provided stream.
|
||||
/// Allows reading a single file from the provided byte array.
|
||||
/// </summary>
|
||||
public class LegacyByteArrayReader : ArchiveReader
|
||||
{
|
||||
|
@ -76,6 +76,7 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft),
|
||||
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
|
||||
new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode),
|
||||
new KeyBinding(new[] { InputKey.F5 }, GlobalAction.EditorTestGameplay),
|
||||
};
|
||||
|
||||
public IEnumerable<KeyBinding> InGameKeyBindings => new[]
|
||||
@ -288,6 +289,9 @@ namespace osu.Game.Input.Bindings
|
||||
ToggleChatFocus,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleGridDisplayMode))]
|
||||
EditorCycleGridDisplayMode
|
||||
EditorCycleGridDisplayMode,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestGameplay))]
|
||||
EditorTestGameplay
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Input.Bindings;
|
||||
@ -16,10 +17,12 @@ namespace osu.Game.Input
|
||||
public class RealmKeyBindingStore
|
||||
{
|
||||
private readonly RealmContextFactory realmFactory;
|
||||
private readonly ReadableKeyCombinationProvider keyCombinationProvider;
|
||||
|
||||
public RealmKeyBindingStore(RealmContextFactory realmFactory)
|
||||
public RealmKeyBindingStore(RealmContextFactory realmFactory, ReadableKeyCombinationProvider keyCombinationProvider)
|
||||
{
|
||||
this.realmFactory = realmFactory;
|
||||
this.keyCombinationProvider = keyCombinationProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -35,7 +38,7 @@ namespace osu.Game.Input
|
||||
{
|
||||
foreach (var action in context.All<RealmKeyBinding>().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction))
|
||||
{
|
||||
string str = action.KeyCombination.ReadableString();
|
||||
string str = keyCombinationProvider.GetReadableString(action.KeyCombination);
|
||||
|
||||
// even if found, the readable string may be empty for an unbound action.
|
||||
if (str.Length > 0)
|
||||
|
@ -169,6 +169,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString EditorCycleGridDisplayMode => new TranslatableString(getKey(@"editor_cycle_grid_display_mode"), @"Cycle grid display mode");
|
||||
|
||||
/// <summary>
|
||||
/// "Test gameplay"
|
||||
/// </summary>
|
||||
public static LocalisableString EditorTestGameplay => new TranslatableString(getKey(@"editor_test_gameplay"), @"Test gameplay");
|
||||
|
||||
/// <summary>
|
||||
/// "Hold for HUD"
|
||||
/// </summary>
|
||||
|
@ -8,20 +8,21 @@ namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class PostBeatmapFavouriteRequest : APIRequest
|
||||
{
|
||||
public readonly BeatmapFavouriteAction Action;
|
||||
|
||||
private readonly int id;
|
||||
private readonly BeatmapFavouriteAction action;
|
||||
|
||||
public PostBeatmapFavouriteRequest(int id, BeatmapFavouriteAction action)
|
||||
{
|
||||
this.id = id;
|
||||
this.action = action;
|
||||
Action = action;
|
||||
}
|
||||
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var req = base.CreateWebRequest();
|
||||
req.Method = HttpMethod.Post;
|
||||
req.AddParameter(@"action", action.ToString().ToLowerInvariant());
|
||||
req.AddParameter(@"action", Action.ToString().ToLowerInvariant());
|
||||
return req;
|
||||
}
|
||||
|
||||
|
@ -61,6 +61,12 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
[JsonProperty(@"track_id")]
|
||||
public int? TrackId { get; set; }
|
||||
|
||||
[JsonProperty(@"hype")]
|
||||
public BeatmapSetHypeStatus? HypeStatus { get; set; }
|
||||
|
||||
[JsonProperty(@"nominations_summary")]
|
||||
public BeatmapSetNominationStatus? NominationStatus { get; set; }
|
||||
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("title_unicode")]
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
|
||||
@ -23,11 +22,6 @@ namespace osu.Game.Online
|
||||
{
|
||||
}
|
||||
|
||||
private IBindable<WeakReference<BeatmapSetInfo>>? managerUpdated;
|
||||
private IBindable<WeakReference<BeatmapSetInfo>>? managerRemoved;
|
||||
private IBindable<WeakReference<ArchiveDownloadRequest<IBeatmapSetInfo>>>? managerDownloadBegan;
|
||||
private IBindable<WeakReference<ArchiveDownloadRequest<IBeatmapSetInfo>>>? managerDownloadFailed;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load()
|
||||
{
|
||||
@ -35,46 +29,30 @@ namespace osu.Game.Online
|
||||
return;
|
||||
|
||||
// Used to interact with manager classes that don't support interface types. Will eventually be replaced.
|
||||
var beatmapSetInfo = new BeatmapSetInfo { OnlineBeatmapSetID = TrackedItem.OnlineID };
|
||||
var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID };
|
||||
|
||||
if (Manager.IsAvailableLocally(beatmapSetInfo))
|
||||
UpdateState(DownloadState.LocallyAvailable);
|
||||
else
|
||||
attachDownload(Manager.GetExistingDownload(beatmapSetInfo));
|
||||
|
||||
managerDownloadBegan = Manager.DownloadBegan.GetBoundCopy();
|
||||
managerDownloadBegan.BindValueChanged(downloadBegan);
|
||||
managerDownloadFailed = Manager.DownloadFailed.GetBoundCopy();
|
||||
managerDownloadFailed.BindValueChanged(downloadFailed);
|
||||
managerUpdated = Manager.ItemUpdated.GetBoundCopy();
|
||||
managerUpdated.BindValueChanged(itemUpdated);
|
||||
managerRemoved = Manager.ItemRemoved.GetBoundCopy();
|
||||
managerRemoved.BindValueChanged(itemRemoved);
|
||||
Manager.DownloadBegan += downloadBegan;
|
||||
Manager.DownloadFailed += downloadFailed;
|
||||
Manager.ItemUpdated += itemUpdated;
|
||||
Manager.ItemRemoved += itemRemoved;
|
||||
}
|
||||
|
||||
private void downloadBegan(ValueChangedEvent<WeakReference<ArchiveDownloadRequest<IBeatmapSetInfo>>> weakRequest)
|
||||
private void downloadBegan(ArchiveDownloadRequest<IBeatmapSetInfo> request) => Schedule(() =>
|
||||
{
|
||||
if (weakRequest.NewValue.TryGetTarget(out var request))
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (checkEquality(request.Model, TrackedItem))
|
||||
attachDownload(request);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (checkEquality(request.Model, TrackedItem))
|
||||
attachDownload(request);
|
||||
});
|
||||
|
||||
private void downloadFailed(ValueChangedEvent<WeakReference<ArchiveDownloadRequest<IBeatmapSetInfo>>> weakRequest)
|
||||
private void downloadFailed(ArchiveDownloadRequest<IBeatmapSetInfo> request) => Schedule(() =>
|
||||
{
|
||||
if (weakRequest.NewValue.TryGetTarget(out var request))
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (checkEquality(request.Model, TrackedItem))
|
||||
attachDownload(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (checkEquality(request.Model, TrackedItem))
|
||||
attachDownload(null);
|
||||
});
|
||||
|
||||
private void attachDownload(ArchiveDownloadRequest<IBeatmapSetInfo>? request)
|
||||
{
|
||||
@ -116,29 +94,17 @@ namespace osu.Game.Online
|
||||
|
||||
private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null));
|
||||
|
||||
private void itemUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem)
|
||||
private void itemUpdated(BeatmapSetInfo item) => Schedule(() =>
|
||||
{
|
||||
if (weakItem.NewValue.TryGetTarget(out var item))
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (checkEquality(item, TrackedItem))
|
||||
UpdateState(DownloadState.LocallyAvailable);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (checkEquality(item, TrackedItem))
|
||||
UpdateState(DownloadState.LocallyAvailable);
|
||||
});
|
||||
|
||||
private void itemRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakItem)
|
||||
private void itemRemoved(BeatmapSetInfo item) => Schedule(() =>
|
||||
{
|
||||
if (weakItem.NewValue.TryGetTarget(out var item))
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (checkEquality(item, TrackedItem))
|
||||
UpdateState(DownloadState.NotDownloaded);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (checkEquality(item, TrackedItem))
|
||||
UpdateState(DownloadState.NotDownloaded);
|
||||
});
|
||||
|
||||
private bool checkEquality(IBeatmapSetInfo x, IBeatmapSetInfo y) => x.OnlineID == y.OnlineID;
|
||||
|
||||
@ -148,6 +114,14 @@ namespace osu.Game.Online
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
attachDownload(null);
|
||||
|
||||
if (Manager != null)
|
||||
{
|
||||
Manager.DownloadBegan -= downloadBegan;
|
||||
Manager.DownloadFailed -= downloadFailed;
|
||||
Manager.ItemUpdated -= itemUpdated;
|
||||
Manager.ItemRemoved -= itemRemoved;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -320,9 +320,9 @@ namespace osu.Game.Online.Chat
|
||||
{
|
||||
public readonly LinkAction Action;
|
||||
|
||||
public readonly string Argument;
|
||||
public readonly object Argument;
|
||||
|
||||
public LinkDetails(LinkAction action, string argument)
|
||||
public LinkDetails(LinkAction action, object argument)
|
||||
{
|
||||
Action = action;
|
||||
Argument = argument;
|
||||
@ -351,9 +351,9 @@ namespace osu.Game.Online.Chat
|
||||
public int Index;
|
||||
public int Length;
|
||||
public LinkAction Action;
|
||||
public string Argument;
|
||||
public object Argument;
|
||||
|
||||
public Link(string url, int startIndex, int length, LinkAction action, string argument)
|
||||
public Link(string url, int startIndex, int length, LinkAction action, object argument)
|
||||
{
|
||||
Url = url;
|
||||
Index = startIndex;
|
||||
|
@ -57,7 +57,7 @@ namespace osu.Game.Online.Chat
|
||||
break;
|
||||
}
|
||||
|
||||
string beatmapString = beatmapInfo.OnlineBeatmapID.HasValue ? $"[{api.WebsiteRootUrl}/b/{beatmapInfo.OnlineBeatmapID} {beatmapInfo}]" : beatmapInfo.ToString();
|
||||
string beatmapString = beatmapInfo.OnlineID.HasValue ? $"[{api.WebsiteRootUrl}/b/{beatmapInfo.OnlineID} {beatmapInfo}]" : beatmapInfo.ToString();
|
||||
|
||||
channelManager.PostMessage($"is {verb} {beatmapString}", true, target);
|
||||
Expire();
|
||||
|
@ -4,6 +4,7 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
@ -161,9 +162,10 @@ namespace osu.Game.Online
|
||||
builder.AddNewtonsoftJsonProtocol(options =>
|
||||
{
|
||||
options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
|
||||
// TODO: This should only be required to be `TypeNameHandling.Auto`.
|
||||
// See usage in osu-server-spectator for further documentation as to why this is required.
|
||||
options.PayloadSerializerSettings.TypeNameHandling = TypeNameHandling.All;
|
||||
options.PayloadSerializerSettings.Converters = new List<JsonConverter>
|
||||
{
|
||||
new SignalRDerivedTypeWorkaroundJsonConverter(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,6 @@ namespace osu.Game.Online.Multiplayer
|
||||
[Serializable]
|
||||
[MessagePackObject]
|
||||
[Union(0, typeof(TeamVersusRoomState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
|
||||
// TODO: abstract breaks json serialisation. attention will be required for iOS support (unless we get messagepack AOT working instead).
|
||||
public abstract class MatchRoomState
|
||||
{
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ namespace osu.Game.Online.Multiplayer
|
||||
[Serializable]
|
||||
[MessagePackObject]
|
||||
[Union(0, typeof(TeamVersusUserState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
|
||||
// TODO: abstract breaks json serialisation. attention will be required for iOS support (unless we get messagepack AOT working instead).
|
||||
public abstract class MatchUserState
|
||||
{
|
||||
}
|
||||
|
@ -229,7 +229,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
Value = new BeatmapInfo
|
||||
{
|
||||
OnlineBeatmapID = Room.Settings.BeatmapID,
|
||||
OnlineID = Room.Settings.BeatmapID,
|
||||
MD5Hash = Room.Settings.BeatmapChecksum
|
||||
}
|
||||
},
|
||||
|
@ -113,7 +113,7 @@ namespace osu.Game.Online.Rooms
|
||||
int onlineId = SelectedItem.Value.Beatmap.Value.OnlineID;
|
||||
string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash;
|
||||
|
||||
return beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == onlineId && b.MD5Hash == checksum && !b.BeatmapSet.DeletePending) != null;
|
||||
return beatmapManager.QueryBeatmap(b => b.OnlineID == onlineId && b.MD5Hash == checksum && !b.BeatmapSet.DeletePending) != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
@ -23,11 +22,6 @@ namespace osu.Game.Online
|
||||
{
|
||||
}
|
||||
|
||||
private IBindable<WeakReference<ScoreInfo>>? managerUpdated;
|
||||
private IBindable<WeakReference<ScoreInfo>>? managerRemoved;
|
||||
private IBindable<WeakReference<ArchiveDownloadRequest<IScoreInfo>>>? managerDownloadBegan;
|
||||
private IBindable<WeakReference<ArchiveDownloadRequest<IScoreInfo>>>? managerDownloadFailed;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load()
|
||||
{
|
||||
@ -46,39 +40,23 @@ namespace osu.Game.Online
|
||||
else
|
||||
attachDownload(Manager.GetExistingDownload(scoreInfo));
|
||||
|
||||
managerDownloadBegan = Manager.DownloadBegan.GetBoundCopy();
|
||||
managerDownloadBegan.BindValueChanged(downloadBegan);
|
||||
managerDownloadFailed = Manager.DownloadFailed.GetBoundCopy();
|
||||
managerDownloadFailed.BindValueChanged(downloadFailed);
|
||||
managerUpdated = Manager.ItemUpdated.GetBoundCopy();
|
||||
managerUpdated.BindValueChanged(itemUpdated);
|
||||
managerRemoved = Manager.ItemRemoved.GetBoundCopy();
|
||||
managerRemoved.BindValueChanged(itemRemoved);
|
||||
Manager.DownloadBegan += downloadBegan;
|
||||
Manager.DownloadFailed += downloadFailed;
|
||||
Manager.ItemUpdated += itemUpdated;
|
||||
Manager.ItemRemoved += itemRemoved;
|
||||
}
|
||||
|
||||
private void downloadBegan(ValueChangedEvent<WeakReference<ArchiveDownloadRequest<IScoreInfo>>> weakRequest)
|
||||
private void downloadBegan(ArchiveDownloadRequest<IScoreInfo> request) => Schedule(() =>
|
||||
{
|
||||
if (weakRequest.NewValue.TryGetTarget(out var request))
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (checkEquality(request.Model, TrackedItem))
|
||||
attachDownload(request);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (checkEquality(request.Model, TrackedItem))
|
||||
attachDownload(request);
|
||||
});
|
||||
|
||||
private void downloadFailed(ValueChangedEvent<WeakReference<ArchiveDownloadRequest<IScoreInfo>>> weakRequest)
|
||||
private void downloadFailed(ArchiveDownloadRequest<IScoreInfo> request) => Schedule(() =>
|
||||
{
|
||||
if (weakRequest.NewValue.TryGetTarget(out var request))
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (checkEquality(request.Model, TrackedItem))
|
||||
attachDownload(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (checkEquality(request.Model, TrackedItem))
|
||||
attachDownload(null);
|
||||
});
|
||||
|
||||
private void attachDownload(ArchiveDownloadRequest<IScoreInfo>? request)
|
||||
{
|
||||
@ -120,29 +98,17 @@ namespace osu.Game.Online
|
||||
|
||||
private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null));
|
||||
|
||||
private void itemUpdated(ValueChangedEvent<WeakReference<ScoreInfo>> weakItem)
|
||||
private void itemUpdated(ScoreInfo item) => Schedule(() =>
|
||||
{
|
||||
if (weakItem.NewValue.TryGetTarget(out var item))
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (checkEquality(item, TrackedItem))
|
||||
UpdateState(DownloadState.LocallyAvailable);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (checkEquality(item, TrackedItem))
|
||||
UpdateState(DownloadState.LocallyAvailable);
|
||||
});
|
||||
|
||||
private void itemRemoved(ValueChangedEvent<WeakReference<ScoreInfo>> weakItem)
|
||||
private void itemRemoved(ScoreInfo item) => Schedule(() =>
|
||||
{
|
||||
if (weakItem.NewValue.TryGetTarget(out var item))
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (checkEquality(item, TrackedItem))
|
||||
UpdateState(DownloadState.NotDownloaded);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (checkEquality(item, TrackedItem))
|
||||
UpdateState(DownloadState.NotDownloaded);
|
||||
});
|
||||
|
||||
private bool checkEquality(IScoreInfo x, IScoreInfo y) => x.OnlineID == y.OnlineID;
|
||||
|
||||
@ -152,6 +118,14 @@ namespace osu.Game.Online
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
attachDownload(null);
|
||||
|
||||
if (Manager != null)
|
||||
{
|
||||
Manager.DownloadBegan -= downloadBegan;
|
||||
Manager.DownloadFailed -= downloadFailed;
|
||||
Manager.ItemUpdated -= itemUpdated;
|
||||
Manager.ItemRemoved -= itemRemoved;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
60
osu.Game/Online/SignalRDerivedTypeWorkaroundJsonConverter.cs
Normal file
60
osu.Game/Online/SignalRDerivedTypeWorkaroundJsonConverter.cs
Normal file
@ -0,0 +1,60 @@
|
||||
// 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 enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace osu.Game.Online
|
||||
{
|
||||
/// <summary>
|
||||
/// A type of <see cref="JsonConverter"/> that serializes a subset of types used in multiplayer/spectator communication that
|
||||
/// derive from a known base type. This is a safe alternative to using <see cref="TypeNameHandling.Auto"/> or <see cref="TypeNameHandling.All"/>,
|
||||
/// which are known to have security issues.
|
||||
/// </summary>
|
||||
public class SignalRDerivedTypeWorkaroundJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType) =>
|
||||
SignalRUnionWorkaroundResolver.BASE_TYPES.Contains(objectType) ||
|
||||
SignalRUnionWorkaroundResolver.DERIVED_TYPES.Contains(objectType);
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? o, JsonSerializer jsonSerializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
|
||||
JObject obj = JObject.Load(reader);
|
||||
|
||||
string type = (string)obj[@"$dtype"]!;
|
||||
|
||||
var resolvedType = SignalRUnionWorkaroundResolver.DERIVED_TYPES.Single(t => t.Name == type);
|
||||
|
||||
object? instance = Activator.CreateInstance(resolvedType);
|
||||
|
||||
jsonSerializer.Populate(obj["$value"]!.CreateReader(), instance);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? o, JsonSerializer serializer)
|
||||
{
|
||||
if (o == null)
|
||||
{
|
||||
writer.WriteNull();
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteStartObject();
|
||||
|
||||
writer.WritePropertyName(@"$dtype");
|
||||
serializer.Serialize(writer, o.GetType().Name);
|
||||
|
||||
writer.WritePropertyName(@"$value");
|
||||
writer.WriteRawValue(JsonConvert.SerializeObject(o));
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
}
|
@ -20,7 +20,22 @@ namespace osu.Game.Online
|
||||
public static readonly MessagePackSerializerOptions OPTIONS =
|
||||
MessagePackSerializerOptions.Standard.WithResolver(new SignalRUnionWorkaroundResolver());
|
||||
|
||||
private static readonly Dictionary<Type, IMessagePackFormatter> formatter_map = new Dictionary<Type, IMessagePackFormatter>
|
||||
public static readonly IReadOnlyList<Type> BASE_TYPES = new[]
|
||||
{
|
||||
typeof(MatchServerEvent),
|
||||
typeof(MatchUserRequest),
|
||||
typeof(MatchRoomState),
|
||||
typeof(MatchUserState),
|
||||
};
|
||||
|
||||
public static readonly IReadOnlyList<Type> DERIVED_TYPES = new[]
|
||||
{
|
||||
typeof(ChangeTeamRequest),
|
||||
typeof(TeamVersusRoomState),
|
||||
typeof(TeamVersusUserState),
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<Type, IMessagePackFormatter> formatter_map = new Dictionary<Type, IMessagePackFormatter>
|
||||
{
|
||||
{ typeof(TeamVersusUserState), new TypeRedirectingFormatter<TeamVersusUserState, MatchUserState>() },
|
||||
{ typeof(TeamVersusRoomState), new TypeRedirectingFormatter<TeamVersusRoomState, MatchRoomState>() },
|
||||
|
@ -144,7 +144,7 @@ namespace osu.Game.Online.Spectator
|
||||
IsPlaying = true;
|
||||
|
||||
// transfer state at point of beginning play
|
||||
currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineBeatmapID;
|
||||
currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID;
|
||||
currentState.RulesetID = score.ScoreInfo.RulesetID;
|
||||
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
|
||||
|
||||
|
@ -289,25 +289,27 @@ namespace osu.Game
|
||||
/// <param name="link">The link to load.</param>
|
||||
public void HandleLink(LinkDetails link) => Schedule(() =>
|
||||
{
|
||||
string argString = link.Argument.ToString();
|
||||
|
||||
switch (link.Action)
|
||||
{
|
||||
case LinkAction.OpenBeatmap:
|
||||
// TODO: proper query params handling
|
||||
if (int.TryParse(link.Argument.Contains('?') ? link.Argument.Split('?')[0] : link.Argument, out int beatmapId))
|
||||
if (int.TryParse(argString.Contains('?') ? argString.Split('?')[0] : argString, out int beatmapId))
|
||||
ShowBeatmap(beatmapId);
|
||||
break;
|
||||
|
||||
case LinkAction.OpenBeatmapSet:
|
||||
if (int.TryParse(link.Argument, out int setId))
|
||||
if (int.TryParse(argString, out int setId))
|
||||
ShowBeatmapSet(setId);
|
||||
break;
|
||||
|
||||
case LinkAction.OpenChannel:
|
||||
ShowChannel(link.Argument);
|
||||
ShowChannel(argString);
|
||||
break;
|
||||
|
||||
case LinkAction.SearchBeatmapSet:
|
||||
SearchBeatmapSet(link.Argument);
|
||||
SearchBeatmapSet(argString);
|
||||
break;
|
||||
|
||||
case LinkAction.OpenEditorTimestamp:
|
||||
@ -321,26 +323,31 @@ namespace osu.Game
|
||||
break;
|
||||
|
||||
case LinkAction.External:
|
||||
OpenUrlExternally(link.Argument);
|
||||
OpenUrlExternally(argString);
|
||||
break;
|
||||
|
||||
case LinkAction.OpenUserProfile:
|
||||
ShowUser(int.TryParse(link.Argument, out int userId)
|
||||
? new APIUser { Id = userId }
|
||||
: new APIUser { Username = link.Argument });
|
||||
if (!(link.Argument is IUser user))
|
||||
{
|
||||
user = int.TryParse(argString, out int userId)
|
||||
? new APIUser { Id = userId }
|
||||
: new APIUser { Username = argString };
|
||||
}
|
||||
|
||||
ShowUser(user);
|
||||
|
||||
break;
|
||||
|
||||
case LinkAction.OpenWiki:
|
||||
ShowWiki(link.Argument);
|
||||
ShowWiki(argString);
|
||||
break;
|
||||
|
||||
case LinkAction.OpenChangelog:
|
||||
if (string.IsNullOrEmpty(link.Argument))
|
||||
if (string.IsNullOrEmpty(argString))
|
||||
ShowChangelogListing();
|
||||
else
|
||||
{
|
||||
string[] changelogArgs = link.Argument.Split("/");
|
||||
string[] changelogArgs = argString.Split("/");
|
||||
ShowChangelogBuild(changelogArgs[0], changelogArgs[1]);
|
||||
}
|
||||
|
||||
@ -436,7 +443,7 @@ namespace osu.Game
|
||||
BeatmapSetInfo databasedSet = null;
|
||||
|
||||
if (beatmap.OnlineID > 0)
|
||||
databasedSet = BeatmapManager.QueryBeatmapSet(s => s.OnlineBeatmapSetID == beatmap.OnlineID);
|
||||
databasedSet = BeatmapManager.QueryBeatmapSet(s => s.OnlineID == beatmap.OnlineID);
|
||||
|
||||
if (beatmap is BeatmapSetInfo localBeatmap)
|
||||
databasedSet ??= BeatmapManager.QueryBeatmapSet(s => s.Hash == localBeatmap.Hash);
|
||||
|
@ -40,6 +40,7 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Stores;
|
||||
using osu.Game.Utils;
|
||||
using RuntimeInfo = osu.Framework.RuntimeInfo;
|
||||
|
||||
@ -64,7 +65,7 @@ namespace osu.Game
|
||||
/// <summary>
|
||||
/// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects.
|
||||
/// </summary>
|
||||
internal const double GLOBAL_TRACK_VOLUME_ADJUST = 0.8;
|
||||
private const double global_track_volume_adjust = 0.8;
|
||||
|
||||
public bool UseDevelopmentServer { get; }
|
||||
|
||||
@ -158,7 +159,9 @@ namespace osu.Game
|
||||
|
||||
private Bindable<bool> fpsDisplayVisible;
|
||||
|
||||
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(GLOBAL_TRACK_VOLUME_ADJUST);
|
||||
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(global_track_volume_adjust);
|
||||
|
||||
private RealmRulesetStore realmRulesetStore;
|
||||
|
||||
public OsuGameBase()
|
||||
{
|
||||
@ -167,7 +170,7 @@ namespace osu.Game
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(ReadableKeyCombinationProvider keyCombinationProvider)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -206,17 +209,11 @@ namespace osu.Game
|
||||
dependencies.CacheAs<ISkinSource>(SkinManager);
|
||||
|
||||
// needs to be done here rather than inside SkinManager to ensure thread safety of CurrentSkinInfo.
|
||||
SkinManager.ItemRemoved.BindValueChanged(weakRemovedInfo =>
|
||||
SkinManager.ItemRemoved += item => Schedule(() =>
|
||||
{
|
||||
if (weakRemovedInfo.NewValue.TryGetTarget(out var removedInfo))
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
// check the removed skin is not the current user choice. if it is, switch back to default.
|
||||
if (removedInfo.ID == SkinManager.CurrentSkinInfo.Value.ID)
|
||||
SkinManager.CurrentSkinInfo.Value = SkinInfo.Default;
|
||||
});
|
||||
}
|
||||
// check the removed skin is not the current user choice. if it is, switch back to default.
|
||||
if (item.ID == SkinManager.CurrentSkinInfo.Value.ID)
|
||||
SkinManager.CurrentSkinInfo.Value = SkinInfo.Default;
|
||||
});
|
||||
|
||||
EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
|
||||
@ -237,6 +234,11 @@ namespace osu.Game
|
||||
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig));
|
||||
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true));
|
||||
|
||||
// the following realm components are not actively used yet, but initialised and kept up to date for initial testing.
|
||||
realmRulesetStore = new RealmRulesetStore(realmFactory, Storage);
|
||||
|
||||
dependencies.Cache(realmRulesetStore);
|
||||
|
||||
// this should likely be moved to ArchiveModelManager when another case appears where it is necessary
|
||||
// to have inter-dependent model managers. this could be obtained with an IHasForeign<T> interface to
|
||||
// allow lookups to be done on the child (ScoreManager in this case) to perform the cascading delete.
|
||||
@ -246,17 +248,8 @@ namespace osu.Game
|
||||
return ScoreManager.QueryScores(s => beatmapIds.Contains(s.BeatmapInfo.ID)).ToList();
|
||||
}
|
||||
|
||||
BeatmapManager.ItemRemoved.BindValueChanged(i =>
|
||||
{
|
||||
if (i.NewValue.TryGetTarget(out var item))
|
||||
ScoreManager.Delete(getBeatmapScores(item), true);
|
||||
});
|
||||
|
||||
BeatmapManager.ItemUpdated.BindValueChanged(i =>
|
||||
{
|
||||
if (i.NewValue.TryGetTarget(out var item))
|
||||
ScoreManager.Undelete(getBeatmapScores(item), true);
|
||||
});
|
||||
BeatmapManager.ItemRemoved += item => ScoreManager.Delete(getBeatmapScores(item), true);
|
||||
BeatmapManager.ItemUpdated += item => ScoreManager.Undelete(getBeatmapScores(item), true);
|
||||
|
||||
dependencies.Cache(difficultyCache = new BeatmapDifficultyCache());
|
||||
AddInternal(difficultyCache);
|
||||
@ -316,13 +309,13 @@ namespace osu.Game
|
||||
|
||||
base.Content.Add(CreateScalingContainer().WithChildren(mainContent));
|
||||
|
||||
KeyBindingStore = new RealmKeyBindingStore(realmFactory);
|
||||
KeyBindingStore = new RealmKeyBindingStore(realmFactory, keyCombinationProvider);
|
||||
KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets);
|
||||
|
||||
dependencies.Cache(globalBindings);
|
||||
|
||||
PreviewTrackManager previewTrackManager;
|
||||
dependencies.Cache(previewTrackManager = new PreviewTrackManager());
|
||||
dependencies.Cache(previewTrackManager = new PreviewTrackManager(BeatmapManager.BeatmapTrackStore));
|
||||
Add(previewTrackManager);
|
||||
|
||||
AddInternal(MusicController = new MusicController());
|
||||
@ -527,6 +520,8 @@ namespace osu.Game
|
||||
LocalConfig?.Dispose();
|
||||
|
||||
contextFactory?.FlushConnections();
|
||||
|
||||
realmRulesetStore?.Dispose();
|
||||
realmFactory?.Dispose();
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
|
||||
case DownloadState.LocallyAvailable:
|
||||
Predicate<BeatmapInfo> findPredicate = null;
|
||||
if (SelectedBeatmap.Value != null)
|
||||
findPredicate = b => b.OnlineBeatmapID == SelectedBeatmap.Value.OnlineID;
|
||||
findPredicate = b => b.OnlineID == SelectedBeatmap.Value.OnlineID;
|
||||
|
||||
game?.PresentBeatmap(beatmapSet, findPredicate);
|
||||
break;
|
||||
|
@ -209,7 +209,7 @@ namespace osu.Game.Overlays.Chat
|
||||
username.Text = $@"{message.Sender.Username}" + (senderHasBackground || message.IsAction ? "" : ":");
|
||||
|
||||
// remove non-existent channels from the link list
|
||||
message.Links.RemoveAll(link => link.Action == LinkAction.OpenChannel && chatManager?.AvailableChannels.Any(c => c.Name == link.Argument) != true);
|
||||
message.Links.RemoveAll(link => link.Action == LinkAction.OpenChannel && chatManager?.AvailableChannels.Any(c => c.Name == link.Argument.ToString()) != true);
|
||||
|
||||
ContentFlow.Clear();
|
||||
ContentFlow.AddLinks(message.DisplayContent, message.Links);
|
||||
|
@ -65,16 +65,11 @@ namespace osu.Game.Overlays
|
||||
[NotNull]
|
||||
public DrawableTrack CurrentTrack { get; private set; } = new DrawableTrack(new TrackVirtual(1000));
|
||||
|
||||
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
|
||||
private IBindable<WeakReference<BeatmapSetInfo>> managerRemoved;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
managerUpdated = beatmaps.ItemUpdated.GetBoundCopy();
|
||||
managerUpdated.BindValueChanged(beatmapUpdated);
|
||||
managerRemoved = beatmaps.ItemRemoved.GetBoundCopy();
|
||||
managerRemoved.BindValueChanged(beatmapRemoved);
|
||||
beatmaps.ItemUpdated += beatmapUpdated;
|
||||
beatmaps.ItemRemoved += beatmapRemoved;
|
||||
|
||||
beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal, true).OrderBy(_ => RNG.Next()));
|
||||
|
||||
@ -110,28 +105,13 @@ namespace osu.Game.Overlays
|
||||
/// </summary>
|
||||
public bool TrackLoaded => CurrentTrack.TrackLoaded;
|
||||
|
||||
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet)
|
||||
private void beatmapUpdated(BeatmapSetInfo set) => Schedule(() =>
|
||||
{
|
||||
if (weakSet.NewValue.TryGetTarget(out var set))
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
beatmapSets.Remove(set);
|
||||
beatmapSets.Add(set);
|
||||
});
|
||||
}
|
||||
}
|
||||
beatmapSets.Remove(set);
|
||||
beatmapSets.Add(set);
|
||||
});
|
||||
|
||||
private void beatmapRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet)
|
||||
{
|
||||
if (weakSet.NewValue.TryGetTarget(out var set))
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
beatmapSets.RemoveAll(s => s.ID == set.ID);
|
||||
});
|
||||
}
|
||||
}
|
||||
private void beatmapRemoved(BeatmapSetInfo set) => Schedule(() => beatmapSets.RemoveAll(s => s.ID == set.ID));
|
||||
|
||||
private ScheduledDelegate seekDelegate;
|
||||
|
||||
@ -437,6 +417,17 @@ namespace osu.Game.Overlays
|
||||
mod.ApplyToTrack(CurrentTrack);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (beatmaps != null)
|
||||
{
|
||||
beatmaps.ItemUpdated -= beatmapUpdated;
|
||||
beatmaps.ItemRemoved -= beatmapRemoved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum TrackChangeDirection
|
||||
|
@ -23,9 +23,16 @@ namespace osu.Game.Overlays.Notifications
|
||||
{
|
||||
private const float loading_spinner_size = 22;
|
||||
|
||||
private LocalisableString text;
|
||||
|
||||
public LocalisableString Text
|
||||
{
|
||||
set => Schedule(() => textDrawable.Text = value);
|
||||
get => text;
|
||||
set
|
||||
{
|
||||
text = value;
|
||||
Schedule(() => textDrawable.Text = text);
|
||||
}
|
||||
}
|
||||
|
||||
public string CompletionText { get; set; } = "Task has completed!";
|
||||
|
@ -216,7 +216,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent
|
||||
private void addBeatmapsetLink()
|
||||
=> content.AddLink(activity.Beatmapset?.Title, LinkAction.OpenBeatmapSet, getLinkArgument(activity.Beatmapset?.Url), creationParameters: t => t.Font = getLinkFont());
|
||||
|
||||
private string getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.APIEndpointUrl}{url}").Argument;
|
||||
private string getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.APIEndpointUrl}{url}").Argument.ToString();
|
||||
|
||||
private FontUsage getLinkFont(FontWeight fontWeight = FontWeight.Regular)
|
||||
=> OsuFont.GetFont(size: font_size, weight: fontWeight, italics: true);
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Database;
|
||||
@ -57,13 +58,16 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
|
||||
public bool FilteringActive { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private ReadableKeyCombinationProvider keyCombinationProvider { get; set; }
|
||||
|
||||
private OsuSpriteText text;
|
||||
private FillFlowContainer cancelAndClearButtons;
|
||||
private FillFlowContainer<KeyButton> buttons;
|
||||
|
||||
private Bindable<bool> isDefault { get; } = new BindableBool(true);
|
||||
|
||||
public IEnumerable<string> FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend(text.Text.ToString());
|
||||
public IEnumerable<string> FilterTerms => bindings.Select(b => keyCombinationProvider.GetReadableString(b.KeyCombination)).Prepend(text.Text.ToString());
|
||||
|
||||
public KeyBindingRow(object action, List<RealmKeyBinding> bindings)
|
||||
{
|
||||
@ -149,12 +153,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
new CancelButton { Action = finalise },
|
||||
new ClearButton { Action = clear },
|
||||
},
|
||||
}
|
||||
},
|
||||
new HoverClickSounds()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
new HoverClickSounds()
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var b in bindings)
|
||||
@ -422,6 +426,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private ReadableKeyCombinationProvider keyCombinationProvider { get; set; }
|
||||
|
||||
private bool isBinding;
|
||||
|
||||
public bool IsBinding
|
||||
@ -470,12 +477,19 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
Margin = new MarginPadding(5),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = keyBinding.KeyCombination.ReadableString(),
|
||||
},
|
||||
new HoverSounds()
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
keyCombinationProvider.KeymapChanged += updateKeyCombinationText;
|
||||
updateKeyCombinationText();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@ -514,7 +528,22 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
return;
|
||||
|
||||
KeyBinding.KeyCombination = newCombination;
|
||||
Text.Text = KeyBinding.KeyCombination.ReadableString();
|
||||
updateKeyCombinationText();
|
||||
}
|
||||
|
||||
private void updateKeyCombinationText()
|
||||
{
|
||||
Scheduler.AddOnce(updateText);
|
||||
|
||||
void updateText() => Text.Text = keyCombinationProvider.GetReadableString(KeyBinding.KeyCombination);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (keyCombinationProvider != null)
|
||||
keyCombinationProvider.KeymapChanged -= updateKeyCombinationText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,9 +56,6 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
[Resolved]
|
||||
private SkinManager skins { get; set; }
|
||||
|
||||
private IBindable<WeakReference<SkinInfo>> managerUpdated;
|
||||
private IBindable<WeakReference<SkinInfo>> managerRemoved;
|
||||
|
||||
[BackgroundDependencyLoader(permitNulls: true)]
|
||||
private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor)
|
||||
{
|
||||
@ -76,11 +73,8 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
new ExportSkinButton(),
|
||||
};
|
||||
|
||||
managerUpdated = skins.ItemUpdated.GetBoundCopy();
|
||||
managerUpdated.BindValueChanged(itemUpdated);
|
||||
|
||||
managerRemoved = skins.ItemRemoved.GetBoundCopy();
|
||||
managerRemoved.BindValueChanged(itemRemoved);
|
||||
skins.ItemUpdated += itemUpdated;
|
||||
skins.ItemRemoved += itemRemoved;
|
||||
|
||||
config.BindWith(OsuSetting.Skin, configBindable);
|
||||
|
||||
@ -129,11 +123,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
skinDropdown.Items = skinItems;
|
||||
}
|
||||
|
||||
private void itemUpdated(ValueChangedEvent<WeakReference<SkinInfo>> weakItem)
|
||||
{
|
||||
if (weakItem.NewValue.TryGetTarget(out var item))
|
||||
Schedule(() => addItem(item));
|
||||
}
|
||||
private void itemUpdated(SkinInfo item) => Schedule(() => addItem(item));
|
||||
|
||||
private void addItem(SkinInfo item)
|
||||
{
|
||||
@ -142,11 +132,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
skinDropdown.Items = newDropdownItems;
|
||||
}
|
||||
|
||||
private void itemRemoved(ValueChangedEvent<WeakReference<SkinInfo>> weakItem)
|
||||
{
|
||||
if (weakItem.NewValue.TryGetTarget(out var item))
|
||||
Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != item.ID).ToArray());
|
||||
}
|
||||
private void itemRemoved(SkinInfo item) => Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != item.ID).ToArray());
|
||||
|
||||
private void sortUserSkins(List<SkinInfo> skinsList)
|
||||
{
|
||||
@ -155,6 +141,17 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
Comparer<SkinInfo>.Create((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (skins != null)
|
||||
{
|
||||
skins.ItemUpdated -= itemUpdated;
|
||||
skins.ItemRemoved -= itemRemoved;
|
||||
}
|
||||
}
|
||||
|
||||
private class SkinSettingsDropdown : SettingsDropdown<SkinInfo>
|
||||
{
|
||||
protected override OsuDropdown<SkinInfo> CreateDropdown() => new SkinDropdownControl();
|
||||
|
@ -6,7 +6,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Overlays.Settings
|
||||
{
|
||||
@ -29,36 +29,26 @@ namespace osu.Game.Overlays.Settings
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
new OsuTextFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = heading,
|
||||
Font = OsuFont.TorusAlternate.With(size: 40),
|
||||
Margin = new MarginPadding
|
||||
{
|
||||
Left = SettingsPanel.CONTENT_MARGINS,
|
||||
Top = Toolbar.Toolbar.TOOLTIP_HEIGHT
|
||||
},
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content2,
|
||||
Text = subheading,
|
||||
Font = OsuFont.GetFont(size: 18),
|
||||
Margin = new MarginPadding
|
||||
{
|
||||
Left = SettingsPanel.CONTENT_MARGINS,
|
||||
Bottom = 30
|
||||
},
|
||||
},
|
||||
Horizontal = SettingsPanel.CONTENT_MARGINS,
|
||||
Top = Toolbar.Toolbar.TOOLTIP_HEIGHT,
|
||||
Bottom = 30
|
||||
}
|
||||
}
|
||||
}.With(flow =>
|
||||
{
|
||||
flow.AddText(heading, header => header.Font = OsuFont.TorusAlternate.With(size: 40));
|
||||
flow.NewLine();
|
||||
flow.AddText(subheading, subheader =>
|
||||
{
|
||||
subheader.Colour = colourProvider.Content2;
|
||||
subheader.Font = OsuFont.GetFont(size: 18);
|
||||
});
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Database;
|
||||
@ -39,6 +40,9 @@ namespace osu.Game.Overlays.Toolbar
|
||||
[Resolved]
|
||||
private TextureStore textures { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private ReadableKeyCombinationProvider keyCombinationProvider { get; set; }
|
||||
|
||||
public void SetIcon(string texture) =>
|
||||
SetIcon(new Sprite
|
||||
{
|
||||
@ -207,7 +211,7 @@ namespace osu.Game.Overlays.Toolbar
|
||||
|
||||
if (realmKeyBinding != null)
|
||||
{
|
||||
string keyBindingString = realmKeyBinding.KeyCombination.ReadableString();
|
||||
string keyBindingString = keyCombinationProvider.GetReadableString(realmKeyBinding.KeyCombination);
|
||||
|
||||
if (!string.IsNullOrEmpty(keyBindingString))
|
||||
keyBindingTooltip.Text = $" ({keyBindingString})";
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks
|
||||
@ -48,10 +49,14 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
yield return new IssueTemplateLowResolution(this).Create(texture.Width, texture.Height);
|
||||
|
||||
string storagePath = context.Beatmap.BeatmapInfo.BeatmapSet.GetPathForFile(backgroundFile);
|
||||
double filesizeMb = context.WorkingBeatmap.GetStream(storagePath).Length / (1024d * 1024d);
|
||||
|
||||
if (filesizeMb > max_filesize_mb)
|
||||
yield return new IssueTemplateTooUncompressed(this).Create(filesizeMb);
|
||||
using (Stream stream = context.WorkingBeatmap.GetStream(storagePath))
|
||||
{
|
||||
double filesizeMb = stream.Length / (1024d * 1024d);
|
||||
|
||||
if (filesizeMb > max_filesize_mb)
|
||||
yield return new IssueTemplateTooUncompressed(this).Create(filesizeMb);
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateTooHighResolution : IssueTemplate
|
||||
|
62
osu.Game/Rulesets/Mods/ModNoScope.cs
Normal file
62
osu.Game/Rulesets/Mods/ModNoScope.cs
Normal file
@ -0,0 +1,62 @@
|
||||
// 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 osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModNoScope : Mod, IApplicableToScoreProcessor, IApplicableToPlayer
|
||||
{
|
||||
public override string Name => "No Scope";
|
||||
public override string Acronym => "NS";
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override IconUsage? Icon => FontAwesome.Solid.EyeSlash;
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
/// <summary>
|
||||
/// Slightly higher than the cutoff for <see cref="Drawable.IsPresent"/>.
|
||||
/// </summary>
|
||||
protected const float MIN_ALPHA = 0.0002f;
|
||||
|
||||
protected const float TRANSITION_DURATION = 100;
|
||||
|
||||
protected BindableNumber<int> CurrentCombo;
|
||||
|
||||
protected IBindable<bool> IsBreakTime;
|
||||
|
||||
protected float ComboBasedAlpha;
|
||||
|
||||
public abstract BindableInt HiddenComboCount { get; }
|
||||
|
||||
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
|
||||
|
||||
public void ApplyToPlayer(Player player)
|
||||
{
|
||||
IsBreakTime = player.IsBreakTime.GetBoundCopy();
|
||||
}
|
||||
|
||||
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
|
||||
{
|
||||
if (HiddenComboCount.Value == 0) return;
|
||||
|
||||
CurrentCombo = scoreProcessor.Combo.GetBoundCopy();
|
||||
CurrentCombo.BindValueChanged(combo =>
|
||||
{
|
||||
ComboBasedAlpha = Math.Max(MIN_ALPHA, 1 - (float)combo.NewValue / HiddenComboCount.Value);
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
public class HiddenComboSlider : OsuSliderBar<int>
|
||||
{
|
||||
public override LocalisableString TooltipText => Current.Value == 0 ? "always hidden" : base.TooltipText;
|
||||
}
|
||||
}
|
15
osu.Game/Rulesets/RulesetLoadException.cs
Normal file
15
osu.Game/Rulesets/RulesetLoadException.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 System;
|
||||
|
||||
namespace osu.Game.Rulesets
|
||||
{
|
||||
public class RulesetLoadException : Exception
|
||||
{
|
||||
public RulesetLoadException(string message)
|
||||
: base(@$"Ruleset could not be loaded ({message})")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,6 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
@ -129,12 +128,15 @@ namespace osu.Game.Rulesets
|
||||
{
|
||||
try
|
||||
{
|
||||
var instanceInfo = ((Ruleset)Activator.CreateInstance(Type.GetType(r.InstantiationInfo).AsNonNull())).RulesetInfo;
|
||||
var resolvedType = Type.GetType(r.InstantiationInfo)
|
||||
?? throw new RulesetLoadException(@"Type could not be resolved");
|
||||
|
||||
var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo
|
||||
?? throw new RulesetLoadException(@"Instantiation failure");
|
||||
|
||||
r.Name = instanceInfo.Name;
|
||||
r.ShortName = instanceInfo.ShortName;
|
||||
r.InstantiationInfo = instanceInfo.InstantiationInfo;
|
||||
|
||||
r.Available = true;
|
||||
}
|
||||
catch
|
||||
|
37
osu.Game/Rulesets/Scoring/HitEventExtensions.cs
Normal file
37
osu.Game/Rulesets/Scoring/HitEventExtensions.cs
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
public static class HitEventExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates the "unstable rate" for a sequence of <see cref="HitEvent"/>s.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A non-null <see langword="double"/> value if unstable rate could be calculated,
|
||||
/// and <see langword="null"/> if unstable rate cannot be calculated due to <paramref name="hitEvents"/> being empty.
|
||||
/// </returns>
|
||||
public static double? CalculateUnstableRate(this IEnumerable<HitEvent> hitEvents)
|
||||
{
|
||||
double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).ToArray();
|
||||
return 10 * standardDeviation(timeOffsets);
|
||||
}
|
||||
|
||||
private static bool affectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit();
|
||||
|
||||
private static double? standardDeviation(double[] timeOffsets)
|
||||
{
|
||||
if (timeOffsets.Length == 0)
|
||||
return null;
|
||||
|
||||
double mean = timeOffsets.Average();
|
||||
double squares = timeOffsets.Select(offset => Math.Pow(offset - mean, 2)).Sum();
|
||||
return Math.Sqrt(squares / timeOffsets.Length);
|
||||
}
|
||||
}
|
||||
}
|
@ -53,6 +53,12 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public readonly Bindable<ScoringMode> Mode = new Bindable<ScoringMode>();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="HitEvent"/>s collected during gameplay thus far.
|
||||
/// Intended for use with various statistics displays.
|
||||
/// </summary>
|
||||
public IReadOnlyList<HitEvent> HitEvents => hitEvents;
|
||||
|
||||
/// <summary>
|
||||
/// The default portion of <see cref="max_score"/> awarded for hitting <see cref="HitObject"/>s accurately. Defaults to 30%.
|
||||
/// </summary>
|
||||
|
@ -225,7 +225,7 @@ namespace osu.Game.Scoring
|
||||
return clone;
|
||||
}
|
||||
|
||||
public override string ToString() => $"{User} playing {BeatmapInfo}";
|
||||
public override string ToString() => this.GetDisplayTitle();
|
||||
|
||||
public bool Equals(ScoreInfo other)
|
||||
{
|
||||
|
15
osu.Game/Scoring/ScoreInfoExtensions.cs
Normal file
15
osu.Game/Scoring/ScoreInfoExtensions.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.Beatmaps;
|
||||
|
||||
namespace osu.Game.Scoring
|
||||
{
|
||||
public static class ScoreInfoExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// A user-presentable display title representing this score.
|
||||
/// </summary>
|
||||
public static string GetDisplayTitle(this IScoreInfo scoreInfo) => $"{scoreInfo.User.Username} playing {scoreInfo.Beatmap.GetDisplayTitle()}";
|
||||
}
|
||||
}
|
@ -246,9 +246,17 @@ namespace osu.Game.Scoring
|
||||
|
||||
#region Implementation of IModelManager<ScoreInfo>
|
||||
|
||||
public IBindable<WeakReference<ScoreInfo>> ItemUpdated => scoreModelManager.ItemUpdated;
|
||||
public event Action<ScoreInfo> ItemUpdated
|
||||
{
|
||||
add => scoreModelManager.ItemUpdated += value;
|
||||
remove => scoreModelManager.ItemUpdated -= value;
|
||||
}
|
||||
|
||||
public IBindable<WeakReference<ScoreInfo>> ItemRemoved => scoreModelManager.ItemRemoved;
|
||||
public event Action<ScoreInfo> ItemRemoved
|
||||
{
|
||||
add => scoreModelManager.ItemRemoved += value;
|
||||
remove => scoreModelManager.ItemRemoved -= value;
|
||||
}
|
||||
|
||||
public Task ImportFromStableAsync(StableStorage stableStorage)
|
||||
{
|
||||
@ -348,11 +356,19 @@ namespace osu.Game.Scoring
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IModelDownloader<ScoreInfo>
|
||||
#region Implementation of IModelDownloader<IScoreInfo>
|
||||
|
||||
public IBindable<WeakReference<ArchiveDownloadRequest<IScoreInfo>>> DownloadBegan => scoreModelDownloader.DownloadBegan;
|
||||
public event Action<ArchiveDownloadRequest<IScoreInfo>> DownloadBegan
|
||||
{
|
||||
add => scoreModelDownloader.DownloadBegan += value;
|
||||
remove => scoreModelDownloader.DownloadBegan -= value;
|
||||
}
|
||||
|
||||
public IBindable<WeakReference<ArchiveDownloadRequest<IScoreInfo>>> DownloadFailed => scoreModelDownloader.DownloadFailed;
|
||||
public event Action<ArchiveDownloadRequest<IScoreInfo>> DownloadFailed
|
||||
{
|
||||
add => scoreModelDownloader.DownloadFailed += value;
|
||||
remove => scoreModelDownloader.DownloadFailed -= value;
|
||||
}
|
||||
|
||||
public bool Download(IScoreInfo model, bool minimiseDownloadSize) =>
|
||||
scoreModelDownloader.Download(model, minimiseDownloadSize);
|
||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Screens.Edit.Components.Menus
|
||||
public BeatmapInfo BeatmapInfo { get; }
|
||||
|
||||
public DifficultyMenuItem(BeatmapInfo beatmapInfo, bool selected, Action<BeatmapInfo> difficultyChangeFunc)
|
||||
: base(beatmapInfo.Version ?? "(unnamed)", null)
|
||||
: base(beatmapInfo.DifficultyName ?? "(unnamed)", null)
|
||||
{
|
||||
BeatmapInfo = beatmapInfo;
|
||||
State.Value = selected;
|
||||
|
@ -0,0 +1,34 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Components.Timelines.Summary
|
||||
{
|
||||
public class TestGameplayButton : OsuButton
|
||||
{
|
||||
protected override SpriteText CreateText() => new OsuSpriteText
|
||||
{
|
||||
Depth = -1,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Font = OsuFont.TorusAlternate.With(weight: FontWeight.Light, size: 24),
|
||||
Shadow = false
|
||||
};
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, OverlayColourProvider colourProvider)
|
||||
{
|
||||
BackgroundColour = colours.Orange1;
|
||||
SpriteText.Colour = colourProvider.Background6;
|
||||
|
||||
Text = "Test!";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
@ -14,19 +16,20 @@ using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit.Timing;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public class DifficultyPointPiece : HitObjectPointPiece, IHasPopover
|
||||
{
|
||||
private readonly HitObject hitObject;
|
||||
public readonly HitObject HitObject;
|
||||
|
||||
private readonly BindableNumber<double> speedMultiplier;
|
||||
|
||||
public DifficultyPointPiece(HitObject hitObject)
|
||||
: base(hitObject.DifficultyControlPoint)
|
||||
{
|
||||
this.hitObject = hitObject;
|
||||
HitObject = hitObject;
|
||||
|
||||
speedMultiplier = hitObject.DifficultyControlPoint.SliderVelocityBindable.GetBoundCopy();
|
||||
}
|
||||
@ -44,14 +47,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
return true;
|
||||
}
|
||||
|
||||
public Popover GetPopover() => new DifficultyEditPopover(hitObject);
|
||||
public Popover GetPopover() => new DifficultyEditPopover(HitObject);
|
||||
|
||||
public class DifficultyEditPopover : OsuPopover
|
||||
{
|
||||
private readonly HitObject hitObject;
|
||||
private readonly DifficultyControlPoint point;
|
||||
|
||||
private SliderWithTextBoxInput<double> sliderVelocitySlider;
|
||||
private IndeterminateSliderWithTextBoxInput<double> sliderVelocitySlider;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private EditorBeatmap beatmap { get; set; }
|
||||
@ -59,7 +61,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
public DifficultyEditPopover(HitObject hitObject)
|
||||
{
|
||||
this.hitObject = hitObject;
|
||||
point = hitObject.DifficultyControlPoint;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -72,11 +73,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
Width = 200,
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0, 15),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
sliderVelocitySlider = new SliderWithTextBoxInput<double>("Velocity")
|
||||
sliderVelocitySlider = new IndeterminateSliderWithTextBoxInput<double>("Velocity", new DifficultyControlPoint().SliderVelocityBindable)
|
||||
{
|
||||
Current = new DifficultyControlPoint().SliderVelocityBindable,
|
||||
KeyboardStep = 0.1f
|
||||
},
|
||||
new OsuTextFlowContainer
|
||||
@ -89,17 +90,37 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
}
|
||||
};
|
||||
|
||||
var selectedPointBindable = point.SliderVelocityBindable;
|
||||
// if the piece belongs to a currently selected object, assume that the user wants to change all selected objects.
|
||||
// if the piece belongs to an unselected object, operate on that object alone, independently of the selection.
|
||||
var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray();
|
||||
var relevantControlPoints = relevantObjects.Select(h => h.DifficultyControlPoint).ToArray();
|
||||
|
||||
// there may be legacy control points, which contain infinite precision for compatibility reasons (see LegacyDifficultyControlPoint).
|
||||
// generally that level of precision could only be set by externally editing the .osu file, so at the point
|
||||
// a user is looking to update this within the editor it should be safe to obliterate this additional precision.
|
||||
double expectedPrecision = new DifficultyControlPoint().SliderVelocityBindable.Precision;
|
||||
if (selectedPointBindable.Precision < expectedPrecision)
|
||||
selectedPointBindable.Precision = expectedPrecision;
|
||||
// even if there are multiple objects selected, we can still display a value if they all have the same value.
|
||||
var selectedPointBindable = relevantControlPoints.Select(point => point.SliderVelocity).Distinct().Count() == 1 ? relevantControlPoints.First().SliderVelocityBindable : null;
|
||||
|
||||
sliderVelocitySlider.Current = selectedPointBindable;
|
||||
sliderVelocitySlider.Current.BindValueChanged(_ => beatmap?.Update(hitObject));
|
||||
if (selectedPointBindable != null)
|
||||
{
|
||||
// there may be legacy control points, which contain infinite precision for compatibility reasons (see LegacyDifficultyControlPoint).
|
||||
// generally that level of precision could only be set by externally editing the .osu file, so at the point
|
||||
// a user is looking to update this within the editor it should be safe to obliterate this additional precision.
|
||||
sliderVelocitySlider.Current.Value = selectedPointBindable.Value;
|
||||
}
|
||||
|
||||
sliderVelocitySlider.Current.BindValueChanged(val =>
|
||||
{
|
||||
if (val.NewValue == null)
|
||||
return;
|
||||
|
||||
beatmap.BeginChange();
|
||||
|
||||
foreach (var h in relevantObjects)
|
||||
{
|
||||
h.DifficultyControlPoint.SliderVelocity = val.NewValue.Value;
|
||||
beatmap.Update(h);
|
||||
}
|
||||
|
||||
beatmap.EndChange();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
@ -14,12 +19,13 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit.Timing;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public class SamplePointPiece : HitObjectPointPiece, IHasPopover
|
||||
{
|
||||
private readonly HitObject hitObject;
|
||||
public readonly HitObject HitObject;
|
||||
|
||||
private readonly Bindable<string> bank;
|
||||
private readonly BindableNumber<int> volume;
|
||||
@ -27,7 +33,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
public SamplePointPiece(HitObject hitObject)
|
||||
: base(hitObject.SampleControlPoint)
|
||||
{
|
||||
this.hitObject = hitObject;
|
||||
HitObject = hitObject;
|
||||
volume = hitObject.SampleControlPoint.SampleVolumeBindable.GetBoundCopy();
|
||||
bank = hitObject.SampleControlPoint.SampleBankBindable.GetBoundCopy();
|
||||
}
|
||||
@ -50,23 +56,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
Label.Text = $"{bank.Value} {volume.Value}";
|
||||
}
|
||||
|
||||
public Popover GetPopover() => new SampleEditPopover(hitObject);
|
||||
public Popover GetPopover() => new SampleEditPopover(HitObject);
|
||||
|
||||
public class SampleEditPopover : OsuPopover
|
||||
{
|
||||
private readonly HitObject hitObject;
|
||||
private readonly SampleControlPoint point;
|
||||
|
||||
private LabelledTextBox bank;
|
||||
private SliderWithTextBoxInput<int> volume;
|
||||
private LabelledTextBox bank = null!;
|
||||
private IndeterminateSliderWithTextBoxInput<int> volume = null!;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private EditorBeatmap beatmap { get; set; }
|
||||
private EditorBeatmap beatmap { get; set; } = null!;
|
||||
|
||||
public SampleEditPopover(HitObject hitObject)
|
||||
{
|
||||
this.hitObject = hitObject;
|
||||
point = hitObject.SampleControlPoint;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -79,25 +83,84 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
Width = 200,
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0, 10),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
bank = new LabelledTextBox
|
||||
{
|
||||
Label = "Bank Name",
|
||||
},
|
||||
volume = new SliderWithTextBoxInput<int>("Volume")
|
||||
{
|
||||
Current = new SampleControlPoint().SampleVolumeBindable,
|
||||
}
|
||||
volume = new IndeterminateSliderWithTextBoxInput<int>("Volume", new SampleControlPoint().SampleVolumeBindable)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bank.Current = point.SampleBankBindable;
|
||||
bank.Current.BindValueChanged(_ => beatmap.Update(hitObject));
|
||||
// if the piece belongs to a currently selected object, assume that the user wants to change all selected objects.
|
||||
// if the piece belongs to an unselected object, operate on that object alone, independently of the selection.
|
||||
var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray();
|
||||
var relevantControlPoints = relevantObjects.Select(h => h.SampleControlPoint).ToArray();
|
||||
|
||||
volume.Current = point.SampleVolumeBindable;
|
||||
volume.Current.BindValueChanged(_ => beatmap.Update(hitObject));
|
||||
// even if there are multiple objects selected, we can still display sample volume or bank if they all have the same value.
|
||||
string? commonBank = getCommonBank(relevantControlPoints);
|
||||
if (!string.IsNullOrEmpty(commonBank))
|
||||
bank.Current.Value = commonBank;
|
||||
|
||||
int? commonVolume = getCommonVolume(relevantControlPoints);
|
||||
if (commonVolume != null)
|
||||
volume.Current.Value = commonVolume.Value;
|
||||
|
||||
updateBankPlaceholderText(relevantObjects);
|
||||
bank.Current.BindValueChanged(val =>
|
||||
{
|
||||
updateBankFor(relevantObjects, val.NewValue);
|
||||
updateBankPlaceholderText(relevantObjects);
|
||||
});
|
||||
// on commit, ensure that the value is correct by sourcing it from the objects' control points again.
|
||||
// this ensures that committing empty text causes a revert to the previous value.
|
||||
bank.OnCommit += (_, __) => bank.Current.Value = getCommonBank(relevantControlPoints);
|
||||
|
||||
volume.Current.BindValueChanged(val => updateVolumeFor(relevantObjects, val.NewValue));
|
||||
}
|
||||
|
||||
private static string? getCommonBank(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleBank).Distinct().Count() == 1 ? relevantControlPoints.First().SampleBank : null;
|
||||
private static int? getCommonVolume(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleVolume).Distinct().Count() == 1 ? (int?)relevantControlPoints.First().SampleVolume : null;
|
||||
|
||||
private void updateBankFor(IEnumerable<HitObject> objects, string? newBank)
|
||||
{
|
||||
if (string.IsNullOrEmpty(newBank))
|
||||
return;
|
||||
|
||||
beatmap.BeginChange();
|
||||
|
||||
foreach (var h in objects)
|
||||
{
|
||||
h.SampleControlPoint.SampleBank = newBank;
|
||||
beatmap.Update(h);
|
||||
}
|
||||
|
||||
beatmap.EndChange();
|
||||
}
|
||||
|
||||
private void updateBankPlaceholderText(IEnumerable<HitObject> objects)
|
||||
{
|
||||
string? commonBank = getCommonBank(objects.Select(h => h.SampleControlPoint).ToArray());
|
||||
bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : null;
|
||||
}
|
||||
|
||||
private void updateVolumeFor(IEnumerable<HitObject> objects, int? newVolume)
|
||||
{
|
||||
if (newVolume == null)
|
||||
return;
|
||||
|
||||
beatmap.BeginChange();
|
||||
|
||||
foreach (var h in objects)
|
||||
{
|
||||
h.SampleControlPoint.SampleVolume = newVolume.Value;
|
||||
beatmap.Update(h);
|
||||
}
|
||||
|
||||
beatmap.EndChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,29 +7,26 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO.Serialization;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose
|
||||
{
|
||||
public class ComposeScreen : EditorScreenWithTimeline, IKeyBindingHandler<PlatformAction>
|
||||
public class ComposeScreen : EditorScreenWithTimeline
|
||||
{
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private EditorClock clock { get; set; }
|
||||
|
||||
private Bindable<string> clipboard { get; set; }
|
||||
|
||||
private HitObjectComposer composer;
|
||||
|
||||
public ComposeScreen()
|
||||
@ -76,18 +73,65 @@ namespace osu.Game.Screens.Edit.Compose
|
||||
return new EditorSkinProvidingContainer(EditorBeatmap).WithChild(content);
|
||||
}
|
||||
|
||||
#region Input Handling
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(EditorClipboard clipboard)
|
||||
{
|
||||
if (e.Action == PlatformAction.Copy)
|
||||
host.GetClipboard()?.SetText(formatSelectionAsString());
|
||||
|
||||
return false;
|
||||
this.clipboard = clipboard.Content.GetBoundCopy();
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
EditorBeatmap.SelectedHitObjects.BindCollectionChanged((_, __) => updateClipboardActionAvailability());
|
||||
clipboard.BindValueChanged(_ => updateClipboardActionAvailability(), true);
|
||||
}
|
||||
|
||||
#region Clipboard operations
|
||||
|
||||
protected override void PerformCut()
|
||||
{
|
||||
base.PerformCut();
|
||||
|
||||
Copy();
|
||||
EditorBeatmap.RemoveRange(EditorBeatmap.SelectedHitObjects.ToArray());
|
||||
}
|
||||
|
||||
protected override void PerformCopy()
|
||||
{
|
||||
base.PerformCopy();
|
||||
|
||||
clipboard.Value = new ClipboardContent(EditorBeatmap).Serialize();
|
||||
|
||||
host.GetClipboard()?.SetText(formatSelectionAsString());
|
||||
}
|
||||
|
||||
protected override void PerformPaste()
|
||||
{
|
||||
base.PerformPaste();
|
||||
|
||||
var objects = clipboard.Value.Deserialize<ClipboardContent>().HitObjects;
|
||||
|
||||
Debug.Assert(objects.Any());
|
||||
|
||||
double timeOffset = clock.CurrentTime - objects.Min(o => o.StartTime);
|
||||
|
||||
foreach (var h in objects)
|
||||
h.StartTime += timeOffset;
|
||||
|
||||
EditorBeatmap.BeginChange();
|
||||
|
||||
EditorBeatmap.SelectedHitObjects.Clear();
|
||||
|
||||
EditorBeatmap.AddRange(objects);
|
||||
EditorBeatmap.SelectedHitObjects.AddRange(objects);
|
||||
|
||||
EditorBeatmap.EndChange();
|
||||
}
|
||||
|
||||
private void updateClipboardActionAvailability()
|
||||
{
|
||||
CanCut.Value = CanCopy.Value = EditorBeatmap.SelectedHitObjects.Any();
|
||||
CanPaste.Value = !string.IsNullOrEmpty(clipboard.Value);
|
||||
}
|
||||
|
||||
private string formatSelectionAsString()
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework;
|
||||
@ -24,7 +23,6 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.IO.Serialization;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@ -33,11 +31,13 @@ using osu.Game.Screens.Edit.Components.Menus;
|
||||
using osu.Game.Screens.Edit.Components.Timelines.Summary;
|
||||
using osu.Game.Screens.Edit.Compose;
|
||||
using osu.Game.Screens.Edit.Design;
|
||||
using osu.Game.Screens.Edit.GameplayTest;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Screens.Edit.Timing;
|
||||
using osu.Game.Screens.Edit.Verify;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -92,6 +92,8 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
private DependencyContainer dependencies;
|
||||
|
||||
private TestGameplayButton testGameplayButton;
|
||||
|
||||
private bool isNewBeatmap;
|
||||
|
||||
protected override UserActivity InitialActivity => new UserActivity.Editing(Beatmap.Value.BeatmapInfo);
|
||||
@ -105,6 +107,12 @@ namespace osu.Game.Screens.Edit
|
||||
[Resolved]
|
||||
private MusicController music { get; set; }
|
||||
|
||||
[Cached]
|
||||
public readonly EditorClipboard Clipboard = new EditorClipboard();
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||
|
||||
public Editor(EditorLoader loader = null)
|
||||
{
|
||||
this.loader = loader;
|
||||
@ -180,10 +188,6 @@ namespace osu.Game.Screens.Edit
|
||||
OsuMenuItem undoMenuItem;
|
||||
OsuMenuItem redoMenuItem;
|
||||
|
||||
EditorMenuItem cutMenuItem;
|
||||
EditorMenuItem copyMenuItem;
|
||||
EditorMenuItem pasteMenuItem;
|
||||
|
||||
AddInternal(new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -265,7 +269,8 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
new Dimension(GridSizeMode.Absolute, 220),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 220)
|
||||
new Dimension(GridSizeMode.Absolute, 220),
|
||||
new Dimension(GridSizeMode.Absolute, 120),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
@ -286,6 +291,13 @@ namespace osu.Game.Screens.Edit
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = 10 },
|
||||
Child = new PlaybackControl { RelativeSizeAxes = Axes.Both },
|
||||
},
|
||||
testGameplayButton = new TestGameplayButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = 10 },
|
||||
Size = new Vector2(1),
|
||||
Action = testGameplay
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -299,24 +311,33 @@ namespace osu.Game.Screens.Edit
|
||||
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
|
||||
editorBeatmap.SelectedHitObjects.BindCollectionChanged((_, __) =>
|
||||
{
|
||||
bool hasObjects = editorBeatmap.SelectedHitObjects.Count > 0;
|
||||
|
||||
cutMenuItem.Action.Disabled = !hasObjects;
|
||||
copyMenuItem.Action.Disabled = !hasObjects;
|
||||
}, true);
|
||||
|
||||
clipboard.BindValueChanged(content => pasteMenuItem.Action.Disabled = string.IsNullOrEmpty(content.NewValue));
|
||||
|
||||
menuBar.Mode.ValueChanged += onModeChanged;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
setUpClipboardActionAvailability();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the beatmap's track has changed, this method must be called to keep the editor in a valid state.
|
||||
/// </summary>
|
||||
public void UpdateClockSource() => clock.ChangeSource(Beatmap.Value.Track);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="EditorState"/> instance representing the current state of the editor.
|
||||
/// </summary>
|
||||
/// <param name="nextBeatmap">
|
||||
/// The next beatmap to be shown, in the case of difficulty switch.
|
||||
/// <see langword="null"/> indicates that the beatmap will not be changing.
|
||||
/// </param>
|
||||
public EditorState GetState([CanBeNull] BeatmapInfo nextBeatmap = null) => new EditorState
|
||||
{
|
||||
Time = clock.CurrentTimeAccurate,
|
||||
ClipboardContent = nextBeatmap == null || editorBeatmap.BeatmapInfo.RulesetID == nextBeatmap.RulesetID ? Clipboard.Content.Value : string.Empty
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Restore the editor to a provided state.
|
||||
/// </summary>
|
||||
@ -324,7 +345,7 @@ namespace osu.Game.Screens.Edit
|
||||
public void RestoreState([NotNull] EditorState state) => Schedule(() =>
|
||||
{
|
||||
clock.Seek(state.Time);
|
||||
clipboard.Value = state.ClipboardContent;
|
||||
Clipboard.Content.Value = state.ClipboardContent;
|
||||
});
|
||||
|
||||
protected void Save()
|
||||
@ -463,6 +484,10 @@ namespace osu.Game.Screens.Edit
|
||||
menuBar.Mode.Value = EditorScreenMode.Verify;
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorTestGameplay:
|
||||
testGameplayButton.TriggerClick();
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@ -475,7 +500,18 @@ namespace osu.Game.Screens.Edit
|
||||
public override void OnEntering(IScreen last)
|
||||
{
|
||||
base.OnEntering(last);
|
||||
dimBackground();
|
||||
resetTrack(true);
|
||||
}
|
||||
|
||||
public override void OnResuming(IScreen last)
|
||||
{
|
||||
base.OnResuming(last);
|
||||
dimBackground();
|
||||
}
|
||||
|
||||
private void dimBackground()
|
||||
{
|
||||
ApplyToBackground(b =>
|
||||
{
|
||||
// todo: temporary. we want to be applying dim using the UserDimContainer eventually.
|
||||
@ -484,8 +520,6 @@ namespace osu.Game.Screens.Edit
|
||||
b.IgnoreUserSettings.Value = true;
|
||||
b.BlurAmount.Value = 0;
|
||||
});
|
||||
|
||||
resetTrack(true);
|
||||
}
|
||||
|
||||
public override bool OnExiting(IScreen next)
|
||||
@ -517,7 +551,21 @@ namespace osu.Game.Screens.Edit
|
||||
ApplyToBackground(b => b.FadeColour(Color4.White, 500));
|
||||
resetTrack();
|
||||
|
||||
// To update the game-wide beatmap with any changes, perform a re-fetch on exit.
|
||||
refetchBeatmap();
|
||||
|
||||
return base.OnExiting(next);
|
||||
}
|
||||
|
||||
public override void OnSuspending(IScreen next)
|
||||
{
|
||||
base.OnSuspending(next);
|
||||
clock.Stop();
|
||||
refetchBeatmap();
|
||||
}
|
||||
|
||||
private void refetchBeatmap()
|
||||
{
|
||||
// To update the game-wide beatmap with any changes, perform a re-fetch on exit/suspend.
|
||||
// This is required as the editor makes its local changes via EditorBeatmap
|
||||
// (which are not propagated outwards to a potentially cached WorkingBeatmap).
|
||||
var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo);
|
||||
@ -527,8 +575,6 @@ namespace osu.Game.Screens.Edit
|
||||
Logger.Log("Editor providing re-fetched beatmap post edit session");
|
||||
Beatmap.Value = refetchedBeatmap;
|
||||
}
|
||||
|
||||
return base.OnExiting(next);
|
||||
}
|
||||
|
||||
private void confirmExitWithSave()
|
||||
@ -561,45 +607,37 @@ namespace osu.Game.Screens.Edit
|
||||
this.Exit();
|
||||
}
|
||||
|
||||
private readonly Bindable<string> clipboard = new Bindable<string>();
|
||||
#region Clipboard support
|
||||
|
||||
protected void Cut()
|
||||
private EditorMenuItem cutMenuItem;
|
||||
private EditorMenuItem copyMenuItem;
|
||||
private EditorMenuItem pasteMenuItem;
|
||||
|
||||
private readonly BindableWithCurrent<bool> canCut = new BindableWithCurrent<bool>();
|
||||
private readonly BindableWithCurrent<bool> canCopy = new BindableWithCurrent<bool>();
|
||||
private readonly BindableWithCurrent<bool> canPaste = new BindableWithCurrent<bool>();
|
||||
|
||||
private void setUpClipboardActionAvailability()
|
||||
{
|
||||
Copy();
|
||||
editorBeatmap.RemoveRange(editorBeatmap.SelectedHitObjects.ToArray());
|
||||
canCut.Current.BindValueChanged(cut => cutMenuItem.Action.Disabled = !cut.NewValue, true);
|
||||
canCopy.Current.BindValueChanged(copy => copyMenuItem.Action.Disabled = !copy.NewValue, true);
|
||||
canPaste.Current.BindValueChanged(paste => pasteMenuItem.Action.Disabled = !paste.NewValue, true);
|
||||
}
|
||||
|
||||
protected void Copy()
|
||||
private void rebindClipboardBindables()
|
||||
{
|
||||
if (editorBeatmap.SelectedHitObjects.Count == 0)
|
||||
return;
|
||||
|
||||
clipboard.Value = new ClipboardContent(editorBeatmap).Serialize();
|
||||
canCut.Current = currentScreen.CanCut;
|
||||
canCopy.Current = currentScreen.CanCopy;
|
||||
canPaste.Current = currentScreen.CanPaste;
|
||||
}
|
||||
|
||||
protected void Paste()
|
||||
{
|
||||
if (string.IsNullOrEmpty(clipboard.Value))
|
||||
return;
|
||||
protected void Cut() => currentScreen?.Cut();
|
||||
|
||||
var objects = clipboard.Value.Deserialize<ClipboardContent>().HitObjects;
|
||||
protected void Copy() => currentScreen?.Copy();
|
||||
|
||||
Debug.Assert(objects.Any());
|
||||
protected void Paste() => currentScreen?.Paste();
|
||||
|
||||
double timeOffset = clock.CurrentTime - objects.Min(o => o.StartTime);
|
||||
|
||||
foreach (var h in objects)
|
||||
h.StartTime += timeOffset;
|
||||
|
||||
editorBeatmap.BeginChange();
|
||||
|
||||
editorBeatmap.SelectedHitObjects.Clear();
|
||||
|
||||
editorBeatmap.AddRange(objects);
|
||||
editorBeatmap.SelectedHitObjects.AddRange(objects);
|
||||
|
||||
editorBeatmap.EndChange();
|
||||
}
|
||||
#endregion
|
||||
|
||||
protected void Undo() => changeHandler.RestoreState(-1);
|
||||
|
||||
@ -677,6 +715,7 @@ namespace osu.Game.Screens.Edit
|
||||
finally
|
||||
{
|
||||
updateSampleDisabledState();
|
||||
rebindClipboardBindables();
|
||||
}
|
||||
}
|
||||
|
||||
@ -737,7 +776,7 @@ namespace osu.Game.Screens.Edit
|
||||
if (difficultyItems.Count > 0)
|
||||
difficultyItems.Add(new EditorMenuItemSpacer());
|
||||
|
||||
foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarDifficulty))
|
||||
foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarRating))
|
||||
difficultyItems.Add(createDifficultyMenuItem(beatmap));
|
||||
}
|
||||
|
||||
@ -754,11 +793,7 @@ namespace osu.Game.Screens.Edit
|
||||
return new DifficultyMenuItem(beatmapInfo, isCurrentDifficulty, SwitchToDifficulty);
|
||||
}
|
||||
|
||||
protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, new EditorState
|
||||
{
|
||||
Time = clock.CurrentTimeAccurate,
|
||||
ClipboardContent = editorBeatmap.BeatmapInfo.RulesetID == nextBeatmap.RulesetID ? clipboard.Value : string.Empty
|
||||
});
|
||||
protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, GetState(nextBeatmap));
|
||||
|
||||
private void cancelExit()
|
||||
{
|
||||
@ -766,6 +801,24 @@ namespace osu.Game.Screens.Edit
|
||||
loader?.CancelPendingDifficultySwitch();
|
||||
}
|
||||
|
||||
private void testGameplay()
|
||||
{
|
||||
if (HasUnsavedChanges)
|
||||
{
|
||||
dialogOverlay.Push(new SaveBeforeGameplayTestDialog(() =>
|
||||
{
|
||||
Save();
|
||||
pushEditorPlayer();
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
pushEditorPlayer();
|
||||
}
|
||||
|
||||
void pushEditorPlayer() => this.Push(new EditorPlayerLoader(this));
|
||||
}
|
||||
|
||||
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
|
||||
|
||||
public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime);
|
||||
|
15
osu.Game/Screens/Edit/EditorClipboard.cs
Normal file
15
osu.Game/Screens/Edit/EditorClipboard.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.Framework.Bindables;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
{
|
||||
/// <summary>
|
||||
/// Wraps the contents of the editor clipboard.
|
||||
/// </summary>
|
||||
public class EditorClipboard
|
||||
{
|
||||
public Bindable<string> Content { get; } = new Bindable<string>();
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
{
|
||||
@ -26,7 +27,7 @@ namespace osu.Game.Screens.Edit
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
base.Content.Add(new Container
|
||||
{
|
||||
@ -41,7 +42,7 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = ColourProvider.Background3,
|
||||
Colour = colourProvider.Background3,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
roundedContent = new Container
|
||||
|
@ -2,10 +2,10 @@
|
||||
// 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.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
{
|
||||
@ -17,9 +17,6 @@ namespace osu.Game.Screens.Edit
|
||||
[Resolved]
|
||||
protected EditorBeatmap EditorBeatmap { get; private set; }
|
||||
|
||||
[Cached]
|
||||
protected readonly OverlayColourProvider ColourProvider;
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
private readonly Container content;
|
||||
|
||||
@ -33,8 +30,6 @@ namespace osu.Game.Screens.Edit
|
||||
Origin = Anchor.Centre;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
ColourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||
|
||||
InternalChild = content = new PopoverContainer { RelativeSizeAxes = Axes.Both };
|
||||
}
|
||||
|
||||
@ -49,5 +44,54 @@ namespace osu.Game.Screens.Edit
|
||||
this.ScaleTo(0.98f, 200, Easing.OutQuint)
|
||||
.FadeOut(200, Easing.OutQuint);
|
||||
}
|
||||
|
||||
#region Clipboard operations
|
||||
|
||||
public BindableBool CanCut { get; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Performs a "cut to clipboard" operation appropriate for the given screen.
|
||||
/// </summary>
|
||||
protected virtual void PerformCut()
|
||||
{
|
||||
}
|
||||
|
||||
public void Cut()
|
||||
{
|
||||
if (CanCut.Value)
|
||||
PerformCut();
|
||||
}
|
||||
|
||||
public BindableBool CanCopy { get; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Performs a "copy to clipboard" operation appropriate for the given screen.
|
||||
/// </summary>
|
||||
protected virtual void PerformCopy()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void Copy()
|
||||
{
|
||||
if (CanCopy.Value)
|
||||
PerformCopy();
|
||||
}
|
||||
|
||||
public BindableBool CanPaste { get; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Performs a "paste from clipboard" operation appropriate for the given screen.
|
||||
/// </summary>
|
||||
protected virtual void PerformPaste()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void Paste()
|
||||
{
|
||||
if (CanPaste.Value)
|
||||
PerformPaste();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
66
osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
Normal file
66
osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
Normal file
@ -0,0 +1,66 @@
|
||||
// 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.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Screens.Edit.GameplayTest
|
||||
{
|
||||
public class EditorPlayer : Player
|
||||
{
|
||||
private readonly Editor editor;
|
||||
private readonly EditorState editorState;
|
||||
|
||||
[Resolved]
|
||||
private MusicController musicController { get; set; }
|
||||
|
||||
public EditorPlayer(Editor editor)
|
||||
: base(new PlayerConfiguration { ShowResults = false })
|
||||
{
|
||||
this.editor = editor;
|
||||
editorState = editor.GetState();
|
||||
}
|
||||
|
||||
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
|
||||
=> new MasterGameplayClockContainer(beatmap, editorState.Time, true);
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
ScoreProcessor.HasCompleted.BindValueChanged(completed =>
|
||||
{
|
||||
if (completed.NewValue)
|
||||
Scheduler.AddDelayed(this.Exit, RESULTS_DISPLAY_DELAY);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void PrepareReplay()
|
||||
{
|
||||
// don't record replays.
|
||||
}
|
||||
|
||||
protected override bool CheckModsAllowFailure() => false; // never fail.
|
||||
|
||||
public override void OnEntering(IScreen last)
|
||||
{
|
||||
base.OnEntering(last);
|
||||
|
||||
// finish alpha transforms on entering to avoid gameplay starting in a half-hidden state.
|
||||
// the finish calls are purposefully not propagated to children to avoid messing up their state.
|
||||
FinishTransforms();
|
||||
GameplayClockContainer.FinishTransforms(false, nameof(Alpha));
|
||||
}
|
||||
|
||||
public override bool OnExiting(IScreen next)
|
||||
{
|
||||
musicController.Stop();
|
||||
|
||||
editorState.Time = GameplayClockContainer.CurrentTime;
|
||||
editor.RestoreState(editorState);
|
||||
return base.OnExiting(next);
|
||||
}
|
||||
}
|
||||
}
|
44
osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs
Normal file
44
osu.Game/Screens/Edit/GameplayTest/EditorPlayerLoader.cs
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Screens.Edit.GameplayTest
|
||||
{
|
||||
public class EditorPlayerLoader : PlayerLoader
|
||||
{
|
||||
[Resolved]
|
||||
private OsuLogo osuLogo { get; set; }
|
||||
|
||||
public EditorPlayerLoader(Editor editor)
|
||||
: base(() => new EditorPlayer(editor))
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnEntering(IScreen last)
|
||||
{
|
||||
base.OnEntering(last);
|
||||
|
||||
MetadataInfo.FinishTransforms(true);
|
||||
}
|
||||
|
||||
protected override void LogoArriving(OsuLogo logo, bool resuming)
|
||||
{
|
||||
// call base with resuming forcefully set to true to reduce logo movements.
|
||||
base.LogoArriving(logo, true);
|
||||
logo.FinishTransforms(true, nameof(Scale));
|
||||
}
|
||||
|
||||
protected override void ContentOut()
|
||||
{
|
||||
base.ContentOut();
|
||||
osuLogo.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override double PlayerPushDelay => 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
// 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 osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Screens.Edit.GameplayTest
|
||||
{
|
||||
public class SaveBeforeGameplayTestDialog : PopupDialog
|
||||
{
|
||||
public SaveBeforeGameplayTestDialog(Action saveAndPreview)
|
||||
{
|
||||
HeaderText = "The beatmap will be saved in order to test it.";
|
||||
|
||||
Icon = FontAwesome.Regular.Save;
|
||||
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogOkButton
|
||||
{
|
||||
Text = "Sounds good, let's go!",
|
||||
Action = saveAndPreview
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = "Oops, continue editing",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -47,7 +47,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
Empty(),
|
||||
|
||||
creatorTextBox = createTextBox<LabelledTextBox>("Creator", metadata.Author.Username),
|
||||
difficultyTextBox = createTextBox<LabelledTextBox>("Difficulty Name", Beatmap.BeatmapInfo.Version),
|
||||
difficultyTextBox = createTextBox<LabelledTextBox>("Difficulty Name", Beatmap.BeatmapInfo.DifficultyName),
|
||||
sourceTextBox = createTextBox<LabelledTextBox>("Source", metadata.Source),
|
||||
tagsTextBox = createTextBox<LabelledTextBox>("Tags", metadata.Tags)
|
||||
};
|
||||
@ -111,7 +111,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
Beatmap.Metadata.Title = RomanisedTitleTextBox.Current.Value;
|
||||
|
||||
Beatmap.Metadata.AuthorString = creatorTextBox.Current.Value;
|
||||
Beatmap.BeatmapInfo.Version = difficultyTextBox.Current.Value;
|
||||
Beatmap.BeatmapInfo.DifficultyName = difficultyTextBox.Current.Value;
|
||||
Beatmap.Metadata.Source = sourceTextBox.Current.Value;
|
||||
Beatmap.Metadata.Tags = tagsTextBox.Current.Value;
|
||||
}
|
||||
|
@ -0,0 +1,123 @@
|
||||
// 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.Globalization;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
/// <summary>
|
||||
/// Analogous to <see cref="SliderWithTextBoxInput{T}"/>, but supports scenarios
|
||||
/// where multiple objects with multiple different property values are selected
|
||||
/// by providing an "indeterminate state".
|
||||
/// </summary>
|
||||
public class IndeterminateSliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T?>
|
||||
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
|
||||
{
|
||||
/// <summary>
|
||||
/// A custom step value for each key press which actuates a change on this control.
|
||||
/// </summary>
|
||||
public float KeyboardStep
|
||||
{
|
||||
get => slider.KeyboardStep;
|
||||
set => slider.KeyboardStep = value;
|
||||
}
|
||||
|
||||
private readonly BindableWithCurrent<T?> current = new BindableWithCurrent<T?>();
|
||||
|
||||
public Bindable<T?> Current
|
||||
{
|
||||
get => current.Current;
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
private readonly SettingsSlider<T> slider;
|
||||
private readonly LabelledTextBox textbox;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="IndeterminateSliderWithTextBoxInput{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="labelText">The label text for the slider and text box.</param>
|
||||
/// <param name="indeterminateValue">
|
||||
/// Bindable to use for the slider until a non-null value is set for <see cref="Current"/>.
|
||||
/// In particular, it can be used to control min/max bounds and precision in the case of <see cref="BindableNumber{T}"/>s.
|
||||
/// </param>
|
||||
public IndeterminateSliderWithTextBoxInput(LocalisableString labelText, Bindable<T> indeterminateValue)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 5),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
textbox = new LabelledTextBox
|
||||
{
|
||||
Label = labelText,
|
||||
},
|
||||
slider = new SettingsSlider<T>
|
||||
{
|
||||
TransferValueOnCommit = true,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Current = indeterminateValue
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
textbox.OnCommit += (t, isNew) =>
|
||||
{
|
||||
if (!isNew) return;
|
||||
|
||||
try
|
||||
{
|
||||
slider.Current.Parse(t.Text);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// TriggerChange below will restore the previous text value on failure.
|
||||
}
|
||||
|
||||
// This is run regardless of parsing success as the parsed number may not actually trigger a change
|
||||
// due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
|
||||
Current.TriggerChange();
|
||||
};
|
||||
slider.Current.BindValueChanged(val => Current.Value = val.NewValue);
|
||||
|
||||
Current.BindValueChanged(_ => updateState(), true);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
if (Current.Value is T nonNullValue)
|
||||
{
|
||||
slider.Current.Value = nonNullValue;
|
||||
|
||||
// use the value from the slider to ensure that any precision/min/max set on it via the initial indeterminate value have been applied correctly.
|
||||
decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo);
|
||||
textbox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}");
|
||||
textbox.PlaceholderText = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
textbox.Text = null;
|
||||
textbox.PlaceholderText = "(multiple)";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -214,10 +215,16 @@ namespace osu.Game.Screens.Menu
|
||||
}
|
||||
else if (!api.IsLoggedIn)
|
||||
{
|
||||
logo.Action += displayLogin;
|
||||
// copy out old action to avoid accidentally capturing logo.Action in closure, causing a self-reference loop.
|
||||
var previousAction = logo.Action;
|
||||
|
||||
// we want to hook into logo.Action to display the login overlay, but also preserve the return value of the old action.
|
||||
// therefore pass the old action to displayLogin, so that it can return that value.
|
||||
// this ensures that the OsuLogo sample does not play when it is not desired.
|
||||
logo.Action = () => displayLogin(previousAction);
|
||||
}
|
||||
|
||||
bool displayLogin()
|
||||
bool displayLogin(Func<bool> originalAction)
|
||||
{
|
||||
if (!loginDisplayed.Value)
|
||||
{
|
||||
@ -225,7 +232,7 @@ namespace osu.Game.Screens.Menu
|
||||
loginDisplayed.Value = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
return originalAction.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,8 +62,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
[Resolved(canBeNull: true)]
|
||||
protected OnlinePlayScreen ParentScreen { get; private set; }
|
||||
|
||||
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
|
||||
|
||||
[Cached]
|
||||
private OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker { get; set; }
|
||||
|
||||
@ -246,8 +244,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
|
||||
SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged));
|
||||
|
||||
managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy();
|
||||
managerUpdated.BindValueChanged(beatmapUpdated);
|
||||
beatmapManager.ItemUpdated += beatmapUpdated;
|
||||
|
||||
UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods));
|
||||
}
|
||||
@ -362,14 +359,14 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
}
|
||||
}
|
||||
|
||||
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet) => Schedule(updateWorkingBeatmap);
|
||||
private void beatmapUpdated(BeatmapSetInfo set) => Schedule(updateWorkingBeatmap);
|
||||
|
||||
private void updateWorkingBeatmap()
|
||||
{
|
||||
var beatmap = SelectedItem.Value?.Beatmap.Value;
|
||||
|
||||
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
|
||||
var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineID);
|
||||
var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID);
|
||||
|
||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
|
||||
}
|
||||
@ -431,6 +428,14 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
/// <param name="room">The room to change the settings of.</param>
|
||||
protected abstract RoomSettingsOverlay CreateRoomSettingsOverlay(Room room);
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (beatmapManager != null)
|
||||
beatmapManager.ItemUpdated -= beatmapUpdated;
|
||||
}
|
||||
|
||||
public class UserModSelectButton : PurpleTriangleButton
|
||||
{
|
||||
}
|
||||
|
@ -29,52 +29,50 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
private OsuClickableContainer clickableContent;
|
||||
|
||||
public TeamDisplay(MultiplayerRoomUser user)
|
||||
{
|
||||
this.user = user;
|
||||
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
Width = 15;
|
||||
|
||||
AutoSizeAxes = Axes.X;
|
||||
|
||||
Margin = new MarginPadding { Horizontal = 3 };
|
||||
|
||||
Alpha = 0;
|
||||
Scale = new Vector2(0, 1);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
box = new Container
|
||||
InternalChild = clickableContent = new OsuClickableContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CornerRadius = 5,
|
||||
Masking = true,
|
||||
Width = 15,
|
||||
Alpha = 0,
|
||||
Scale = new Vector2(0, 1),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Child = new Box
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Child = box = new Container
|
||||
{
|
||||
Colour = Color4.White,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CornerRadius = 5,
|
||||
Masking = true,
|
||||
Scale = new Vector2(0, 1),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Child = new Box
|
||||
{
|
||||
Colour = Color4.White,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (Client.LocalUser?.Equals(user) == true)
|
||||
{
|
||||
InternalChild = new OsuClickableContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
TooltipText = "Change team",
|
||||
Action = changeTeam,
|
||||
Child = box
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
InternalChild = box;
|
||||
clickableContent.Action = changeTeam;
|
||||
clickableContent.TooltipText = "Change team";
|
||||
}
|
||||
|
||||
sampleTeamSwap = audio.Samples.Get(@"Multiplayer/team-swap");
|
||||
@ -88,7 +86,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
});
|
||||
}
|
||||
|
||||
private int? displayedTeam;
|
||||
public int? DisplayedTeam { get; private set; }
|
||||
|
||||
protected override void OnRoomUpdated()
|
||||
{
|
||||
@ -102,28 +100,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
|
||||
int? newTeam = (userRoomState as TeamVersusUserState)?.TeamID;
|
||||
|
||||
if (newTeam == displayedTeam)
|
||||
if (newTeam == DisplayedTeam)
|
||||
return;
|
||||
|
||||
// only play the sample if an already valid team changes to another valid team.
|
||||
// this avoids playing a sound for each user if the match type is changed to/from a team mode.
|
||||
if (newTeam != null && displayedTeam != null)
|
||||
if (newTeam != null && DisplayedTeam != null)
|
||||
sampleTeamSwap?.Play();
|
||||
|
||||
displayedTeam = newTeam;
|
||||
DisplayedTeam = newTeam;
|
||||
|
||||
if (displayedTeam != null)
|
||||
if (DisplayedTeam != null)
|
||||
{
|
||||
box.FadeColour(getColourForTeam(displayedTeam.Value), duration, Easing.OutQuint);
|
||||
box.FadeColour(getColourForTeam(DisplayedTeam.Value), duration, Easing.OutQuint);
|
||||
box.ScaleTo(new Vector2(box.Scale.X < 0 ? 1 : -1, 1), duration, Easing.OutQuint);
|
||||
|
||||
this.ScaleTo(Vector2.One, duration, Easing.OutQuint);
|
||||
this.FadeIn(duration);
|
||||
clickableContent.ScaleTo(Vector2.One, duration, Easing.OutQuint);
|
||||
clickableContent.FadeIn(duration);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ScaleTo(new Vector2(0, 1), duration, Easing.OutQuint);
|
||||
this.FadeOut(duration);
|
||||
clickableContent.ScaleTo(new Vector2(0, 1), duration, Easing.OutQuint);
|
||||
clickableContent.FadeOut(duration);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
private void load(IBindable<RulesetInfo> ruleset)
|
||||
{
|
||||
// Sanity checks to ensure that PlaylistsPlayer matches the settings for the current PlaylistItem
|
||||
if (Beatmap.Value.BeatmapInfo.OnlineBeatmapID != PlaylistItem.Beatmap.Value.OnlineID)
|
||||
if (Beatmap.Value.BeatmapInfo.OnlineID != PlaylistItem.Beatmap.Value.OnlineID)
|
||||
throw new InvalidOperationException("Current Beatmap does not match PlaylistItem's Beatmap");
|
||||
|
||||
if (ruleset.Value.ID != PlaylistItem.Ruleset.Value.ID)
|
||||
|
@ -126,7 +126,7 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = beatmap?.BeatmapInfo?.Version,
|
||||
Text = beatmap?.BeatmapInfo?.DifficultyName,
|
||||
Font = OsuFont.GetFont(size: 26, italics: true),
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
|
@ -107,7 +107,8 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ =>
|
||||
{
|
||||
RemoveFilters();
|
||||
// Don't reset frequency as the pause screen may appear post transform, causing a second frequency sweep.
|
||||
RemoveFilters(false);
|
||||
OnComplete?.Invoke();
|
||||
});
|
||||
|
||||
@ -137,15 +138,16 @@ namespace osu.Game.Screens.Play
|
||||
Content.FadeColour(Color4.Gray, duration);
|
||||
}
|
||||
|
||||
public void RemoveFilters()
|
||||
public void RemoveFilters(bool resetTrackFrequency = true)
|
||||
{
|
||||
if (resetTrackFrequency)
|
||||
track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq);
|
||||
|
||||
if (filters.Parent == null)
|
||||
return;
|
||||
|
||||
RemoveInternal(filters);
|
||||
filters.Dispose();
|
||||
|
||||
track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user