mirror of
https://github.com/osukey/osukey.git
synced 2025-08-04 23:24:04 +09:00
Merge branch 'master' into fix_progress_bar_info
This commit is contained in:
@ -105,5 +105,11 @@ namespace osu.Game.Audio
|
||||
/// Retrieves the audio track.
|
||||
/// </summary>
|
||||
protected abstract Track? GetTrack();
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
Track?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
18
osu.Game/Beatmaps/BeatSyncProviderExtensions.cs
Normal file
18
osu.Game/Beatmaps/BeatSyncProviderExtensions.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.
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
public static class BeatSyncProviderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Check whether beat sync is currently available.
|
||||
/// </summary>
|
||||
public static bool CheckBeatSyncAvailable(this IBeatSyncProvider provider) => provider.Clock != null;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the beat sync provider is currently in a kiai section. Should make everything more epic.
|
||||
/// </summary>
|
||||
public static bool CheckIsKiaiTime(this IBeatSyncProvider provider) => provider.Clock != null && provider.ControlPoints?.EffectPointAt(provider.Clock.CurrentTime).KiaiMode == true;
|
||||
}
|
||||
}
|
@ -102,6 +102,14 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public string OnlineMD5Hash { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The last time of a local modification (via the editor).
|
||||
/// </summary>
|
||||
public DateTimeOffset? LastLocalUpdate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The last time online metadata was applied to this beatmap.
|
||||
/// </summary>
|
||||
public DateTimeOffset? LastOnlineUpdate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@ -121,7 +129,8 @@ namespace osu.Game.Beatmaps
|
||||
OnlineID = -1;
|
||||
LastOnlineUpdate = null;
|
||||
OnlineMD5Hash = string.Empty;
|
||||
Status = BeatmapOnlineStatus.None;
|
||||
if (Status != BeatmapOnlineStatus.LocallyModified)
|
||||
Status = BeatmapOnlineStatus.None;
|
||||
}
|
||||
|
||||
#region Properties we may not want persisted (but also maybe no harm?)
|
||||
@ -198,8 +207,8 @@ namespace osu.Game.Beatmaps
|
||||
Debug.Assert(x.BeatmapSet != null);
|
||||
Debug.Assert(y.BeatmapSet != null);
|
||||
|
||||
string? fileHashX = x.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(x.Metadata))?.File.Hash;
|
||||
string? fileHashY = y.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(y.Metadata))?.File.Hash;
|
||||
string? fileHashX = x.BeatmapSet.GetFile(getFilename(x.Metadata))?.File.Hash;
|
||||
string? fileHashY = y.BeatmapSet.GetFile(getFilename(y.Metadata))?.File.Hash;
|
||||
|
||||
return fileHashX == fileHashY;
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
|
@ -94,6 +94,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
var beatmapSet = new BeatmapSetInfo
|
||||
{
|
||||
DateAdded = DateTimeOffset.UtcNow,
|
||||
Beatmaps =
|
||||
{
|
||||
new BeatmapInfo(ruleset, new BeatmapDifficulty(), metadata)
|
||||
@ -300,7 +301,7 @@ namespace osu.Game.Beatmaps
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
// AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
|
||||
var existingFileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase));
|
||||
var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null;
|
||||
string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo);
|
||||
|
||||
// ensure that two difficulties from the set don't point at the same beatmap file.
|
||||
@ -313,9 +314,13 @@ namespace osu.Game.Beatmaps
|
||||
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
|
||||
beatmapInfo.Hash = stream.ComputeSHA2Hash();
|
||||
|
||||
beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
|
||||
beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
|
||||
|
||||
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
|
||||
|
||||
setInfo.Hash = beatmapImporter.ComputeHash(setInfo);
|
||||
setInfo.Status = BeatmapOnlineStatus.LocallyModified;
|
||||
|
||||
Realm.Write(r =>
|
||||
{
|
||||
|
@ -3,13 +3,23 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
public enum BeatmapOnlineStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a special status given when local changes are made via the editor.
|
||||
/// Once in this state, online status changes should be ignored unless the beatmap is reverted or submitted.
|
||||
/// </summary>
|
||||
[Description("Local")]
|
||||
[LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.LocallyModified))]
|
||||
LocallyModified = -4,
|
||||
|
||||
None = -3,
|
||||
|
||||
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusGraveyard))]
|
||||
|
@ -84,13 +84,6 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
|
||||
/// The path returned is relative to the user file storage.
|
||||
/// </summary>
|
||||
/// <param name="filename">The name of the file to get the storage path of.</param>
|
||||
public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath();
|
||||
|
||||
public bool Equals(BeatmapSetInfo? other)
|
||||
{
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
|
33
osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs
Normal file
33
osu.Game/Beatmaps/BeatmapSetInfoExtensions.cs
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Models;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
public static class BeatmapSetInfoExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
|
||||
/// The path returned is relative to the user file storage.
|
||||
/// The lookup is case insensitive.
|
||||
/// </summary>
|
||||
/// <param name="model">The model to operate on.</param>
|
||||
/// <param name="filename">The name of the file to get the storage path of.</param>
|
||||
public static string? GetPathForFile(this IHasRealmFiles model, string filename) => model.GetFile(filename)?.File.GetStoragePath();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file usage for the file in this beatmapset with the given filename, if any exists, otherwise null.
|
||||
/// The path returned is relative to the user file storage.
|
||||
/// The lookup is case insensitive.
|
||||
/// </summary>
|
||||
/// <param name="model">The model to operate on.</param>
|
||||
/// <param name="filename">The name of the file to get the storage path of.</param>
|
||||
public static RealmNamedFileUsage? GetFile(this IHasRealmFiles model, string filename) =>
|
||||
model.Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using osu.Framework.Development;
|
||||
@ -51,6 +52,9 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// Queue an update for a beatmap set.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This may happen during initial import, or at a later stage in response to a user action or server event.
|
||||
/// </remarks>
|
||||
/// <param name="beatmapSet">The beatmap set to update. Updates will be applied directly (so a transaction should be started if this instance is managed).</param>
|
||||
/// <param name="preferOnlineFetch">Whether metadata from an online source should be preferred. If <c>true</c>, the local cache will be skipped to ensure the freshest data state possible.</param>
|
||||
public void Update(BeatmapSetInfo beatmapSet, bool preferOnlineFetch)
|
||||
@ -89,21 +93,26 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
if (res != null)
|
||||
{
|
||||
beatmapInfo.Status = res.Status;
|
||||
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapOnlineStatus.None;
|
||||
beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID;
|
||||
beatmapInfo.BeatmapSet.DateRanked = res.BeatmapSet?.Ranked;
|
||||
beatmapInfo.BeatmapSet.DateSubmitted = res.BeatmapSet?.Submitted;
|
||||
|
||||
beatmapInfo.OnlineID = res.OnlineID;
|
||||
beatmapInfo.OnlineMD5Hash = res.MD5Hash;
|
||||
beatmapInfo.LastOnlineUpdate = res.LastUpdated;
|
||||
|
||||
beatmapInfo.OnlineID = res.OnlineID;
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID;
|
||||
|
||||
beatmapInfo.Metadata.Author.OnlineID = res.AuthorID;
|
||||
// Some metadata should only be applied if there's no local changes.
|
||||
if (shouldSaveOnlineMetadata(beatmapInfo))
|
||||
{
|
||||
beatmapInfo.Status = res.Status;
|
||||
beatmapInfo.Metadata.Author.OnlineID = res.AuthorID;
|
||||
}
|
||||
|
||||
if (beatmapInfo.BeatmapSet.Beatmaps.All(shouldSaveOnlineMetadata))
|
||||
{
|
||||
beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapOnlineStatus.None;
|
||||
beatmapInfo.BeatmapSet.DateRanked = res.BeatmapSet?.Ranked;
|
||||
beatmapInfo.BeatmapSet.DateSubmitted = res.BeatmapSet?.Submitted;
|
||||
}
|
||||
|
||||
logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineID}.");
|
||||
}
|
||||
@ -202,19 +211,26 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
var status = (BeatmapOnlineStatus)reader.GetByte(2);
|
||||
|
||||
beatmapInfo.Status = status;
|
||||
// Some metadata should only be applied if there's no local changes.
|
||||
if (shouldSaveOnlineMetadata(beatmapInfo))
|
||||
{
|
||||
beatmapInfo.Status = status;
|
||||
beatmapInfo.Metadata.Author.OnlineID = reader.GetInt32(3);
|
||||
}
|
||||
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
beatmapInfo.BeatmapSet.Status = status;
|
||||
beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0);
|
||||
// TODO: DateSubmitted and DateRanked are not provided by local cache.
|
||||
beatmapInfo.OnlineID = reader.GetInt32(1);
|
||||
beatmapInfo.Metadata.Author.OnlineID = reader.GetInt32(3);
|
||||
|
||||
beatmapInfo.OnlineMD5Hash = reader.GetString(4);
|
||||
beatmapInfo.LastOnlineUpdate = reader.GetDateTimeOffset(5);
|
||||
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0);
|
||||
|
||||
if (beatmapInfo.BeatmapSet.Beatmaps.All(shouldSaveOnlineMetadata))
|
||||
{
|
||||
beatmapInfo.BeatmapSet.Status = status;
|
||||
}
|
||||
|
||||
logForModel(set, $"Cached local retrieval for {beatmapInfo}.");
|
||||
return true;
|
||||
}
|
||||
@ -233,6 +249,12 @@ namespace osu.Game.Beatmaps
|
||||
private void logForModel(BeatmapSetInfo set, string message) =>
|
||||
RealmArchiveModelImporter<BeatmapSetInfo>.LogForModel(set, $"[{nameof(BeatmapUpdaterMetadataLookup)}] {message}");
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the provided beatmap is in a state where online "ranked" status metadata should be saved against it.
|
||||
/// Handles the case where a user may have locally modified a beatmap in the editor and expects the local status to stick.
|
||||
/// </summary>
|
||||
private static bool shouldSaveOnlineMetadata(BeatmapInfo beatmapInfo) => beatmapInfo.MatchesOnlineVersion || beatmapInfo.Status != BeatmapOnlineStatus.LocallyModified;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
cacheDownloadRequest?.Dispose();
|
||||
|
@ -6,15 +6,18 @@ using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Beatmaps.Drawables
|
||||
{
|
||||
public class BeatmapSetOnlineStatusPill : CircularContainer
|
||||
public class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip
|
||||
{
|
||||
private BeatmapOnlineStatus status;
|
||||
|
||||
@ -96,5 +99,19 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
|
||||
background.Colour = OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter;
|
||||
}
|
||||
|
||||
public LocalisableString TooltipText
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (Status)
|
||||
{
|
||||
case BeatmapOnlineStatus.LocallyModified:
|
||||
return SongSelectStrings.LocallyModifiedTooltip;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -118,7 +118,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
|
||||
// another async load might have completed before this one.
|
||||
// if so, do not make any changes.
|
||||
if (loadedPreview != previewTrack)
|
||||
{
|
||||
loadedPreview.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
AddInternal(loadedPreview);
|
||||
toggleLoading(false);
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Containers;
|
||||
@ -14,12 +14,16 @@ namespace osu.Game.Beatmaps
|
||||
/// Primarily intended for use with <see cref="BeatSyncedContainer"/>.
|
||||
/// </summary>
|
||||
[Cached]
|
||||
public interface IBeatSyncProvider
|
||||
public interface IBeatSyncProvider : IHasAmplitudes
|
||||
{
|
||||
/// <summary>
|
||||
/// Access any available control points from a beatmap providing beat sync. If <c>null</c>, no current provider is available.
|
||||
/// </summary>
|
||||
ControlPointInfo? ControlPoints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Access a clock currently responsible for providing beat sync. If <c>null</c>, no current provider is available.
|
||||
/// </summary>
|
||||
IClock? Clock { get; }
|
||||
|
||||
ChannelAmplitudes? Amplitudes { get; }
|
||||
}
|
||||
}
|
||||
|
@ -134,6 +134,6 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
|
||||
/// </summary>
|
||||
void PrepareTrackForPreviewLooping();
|
||||
void PrepareTrackForPreview(bool looping);
|
||||
}
|
||||
}
|
||||
|
@ -110,9 +110,9 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
|
||||
|
||||
public void PrepareTrackForPreviewLooping()
|
||||
public void PrepareTrackForPreview(bool looping)
|
||||
{
|
||||
Track.Looping = true;
|
||||
Track.Looping = looping;
|
||||
Track.RestartPoint = Metadata.PreviewTime;
|
||||
|
||||
if (Track.RestartPoint == -1)
|
||||
|
@ -9,6 +9,8 @@ using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Dummy;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Lists;
|
||||
@ -56,7 +58,7 @@ namespace osu.Game.Beatmaps
|
||||
this.resources = resources;
|
||||
this.host = host;
|
||||
this.files = files;
|
||||
largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(files));
|
||||
largeTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), host?.CreateTextureLoaderStore(files));
|
||||
this.trackStore = trackStore;
|
||||
}
|
||||
|
||||
@ -110,6 +112,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore;
|
||||
ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
|
||||
IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer();
|
||||
AudioManager IStorageResourceProvider.AudioManager => audioManager;
|
||||
RealmAccess IStorageResourceProvider.RealmAccess => null;
|
||||
IResourceStore<byte[]> IStorageResourceProvider.Files => files;
|
||||
|
@ -3,33 +3,17 @@
|
||||
|
||||
using System;
|
||||
using Humanizer;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Collections
|
||||
{
|
||||
public class DeleteCollectionDialog : PopupDialog
|
||||
public class DeleteCollectionDialog : DeleteConfirmationDialog
|
||||
{
|
||||
public DeleteCollectionDialog(Live<BeatmapCollection> collection, Action deleteAction)
|
||||
{
|
||||
HeaderText = "Confirm deletion of";
|
||||
BodyText = collection.PerformRead(c => $"{c.Name} ({"beatmap".ToQuantity(c.BeatmapMD5Hashes.Count)})");
|
||||
|
||||
Icon = FontAwesome.Regular.TrashAlt;
|
||||
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogOkButton
|
||||
{
|
||||
Text = @"Yes. Go for it.",
|
||||
Action = deleteAction
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = @"No! Abort mission!",
|
||||
},
|
||||
};
|
||||
DeleteAction = deleteAction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,16 +3,20 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
public enum BackgroundSource
|
||||
{
|
||||
[LocalisableDescription(typeof(SkinSettingsStrings), nameof(SkinSettingsStrings.SkinSectionHeader))]
|
||||
Skin,
|
||||
|
||||
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.BeatmapHeader))]
|
||||
Beatmap,
|
||||
|
||||
[Description("Beatmap (with storyboard / video)")]
|
||||
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.BeatmapWithStoryboard))]
|
||||
BeatmapWithStoryboard,
|
||||
}
|
||||
}
|
||||
|
@ -3,17 +3,20 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
public enum DiscordRichPresenceMode
|
||||
{
|
||||
[LocalisableDescription(typeof(OnlineSettingsStrings), nameof(OnlineSettingsStrings.DiscordPresenceOff))]
|
||||
Off,
|
||||
|
||||
[Description("Hide identifiable information")]
|
||||
[LocalisableDescription(typeof(OnlineSettingsStrings), nameof(OnlineSettingsStrings.HideIdentifiableInformation))]
|
||||
Limited,
|
||||
|
||||
[LocalisableDescription(typeof(OnlineSettingsStrings), nameof(OnlineSettingsStrings.DiscordPresenceFull))]
|
||||
Full
|
||||
}
|
||||
}
|
||||
|
@ -3,17 +3,20 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
public enum HUDVisibilityMode
|
||||
{
|
||||
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.NeverShowHUD))]
|
||||
Never,
|
||||
|
||||
[Description("Hide during gameplay")]
|
||||
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.HideDuringGameplay))]
|
||||
HideDuringGameplay,
|
||||
|
||||
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.AlwaysShowHUD))]
|
||||
Always
|
||||
}
|
||||
}
|
||||
|
@ -3,16 +3,17 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
public enum RandomSelectAlgorithm
|
||||
{
|
||||
[Description("Never repeat")]
|
||||
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.NeverRepeat))]
|
||||
RandomPermutation,
|
||||
|
||||
[Description("True Random")]
|
||||
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.TrueRandom))]
|
||||
Random
|
||||
}
|
||||
}
|
||||
|
@ -3,17 +3,23 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
public enum ScalingMode
|
||||
{
|
||||
[LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScalingOff))]
|
||||
Off,
|
||||
|
||||
[LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScaleEverything))]
|
||||
Everything,
|
||||
|
||||
[Description("Excluding overlays")]
|
||||
[LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScaleEverythingExcludingOverlays))]
|
||||
ExcludeOverlays,
|
||||
|
||||
[LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScaleGameplay))]
|
||||
Gameplay,
|
||||
}
|
||||
}
|
||||
|
@ -3,16 +3,17 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
public enum ScreenshotFormat
|
||||
{
|
||||
[Description("JPG (web-friendly)")]
|
||||
[LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.Jpg))]
|
||||
Jpg = 1,
|
||||
|
||||
[Description("PNG (lossless)")]
|
||||
[LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.Png))]
|
||||
Png = 2
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,9 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
public enum SeasonalBackgroundMode
|
||||
@ -10,16 +13,19 @@ namespace osu.Game.Configuration
|
||||
/// <summary>
|
||||
/// Seasonal backgrounds are shown regardless of season, if at all available.
|
||||
/// </summary>
|
||||
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.AlwaysSeasonalBackground))]
|
||||
Always,
|
||||
|
||||
/// <summary>
|
||||
/// Seasonal backgrounds are shown only during their corresponding season.
|
||||
/// </summary>
|
||||
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.SometimesSeasonalBackground))]
|
||||
Sometimes,
|
||||
|
||||
/// <summary>
|
||||
/// Seasonal backgrounds are never shown.
|
||||
/// </summary>
|
||||
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.NeverSeasonalBackground))]
|
||||
Never
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
@ -43,12 +44,47 @@ namespace osu.Game.Configuration
|
||||
/// </remarks>
|
||||
public Type? SettingControlType { get; set; }
|
||||
|
||||
public SettingSourceAttribute(Type declaringType, string label, string? description = null)
|
||||
{
|
||||
Label = getLocalisableStringFromMember(label) ?? string.Empty;
|
||||
Description = getLocalisableStringFromMember(description) ?? string.Empty;
|
||||
|
||||
LocalisableString? getLocalisableStringFromMember(string? member)
|
||||
{
|
||||
if (member == null)
|
||||
return null;
|
||||
|
||||
var property = declaringType.GetMember(member, BindingFlags.Static | BindingFlags.Public).FirstOrDefault();
|
||||
|
||||
if (property == null)
|
||||
return null;
|
||||
|
||||
switch (property)
|
||||
{
|
||||
case FieldInfo f:
|
||||
return (LocalisableString)f.GetValue(null).AsNonNull();
|
||||
|
||||
case PropertyInfo p:
|
||||
return (LocalisableString)p.GetValue(null).AsNonNull();
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException($"Member \"{member}\" was not found in type {declaringType} (must be a static field or property)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SettingSourceAttribute(string? label, string? description = null)
|
||||
{
|
||||
Label = label ?? string.Empty;
|
||||
Description = description ?? string.Empty;
|
||||
}
|
||||
|
||||
public SettingSourceAttribute(Type declaringType, string label, string description, int orderPosition)
|
||||
: this(declaringType, label, description)
|
||||
{
|
||||
OrderPosition = orderPosition;
|
||||
}
|
||||
|
||||
public SettingSourceAttribute(string label, string description, int orderPosition)
|
||||
: this(label, description)
|
||||
{
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Models;
|
||||
|
||||
namespace osu.Game.Database
|
||||
@ -11,8 +12,16 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
public interface IHasRealmFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Available files in this model, with locally filenames.
|
||||
/// When performing lookups, consider using <see cref="BeatmapSetInfoExtensions.GetFile"/> or <see cref="BeatmapSetInfoExtensions.GetPathForFile"/> to do case-insensitive lookups.
|
||||
/// </summary>
|
||||
IList<RealmNamedFileUsage> Files { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A combined hash representing the model, based on the files it contains.
|
||||
/// Implementation specific.
|
||||
/// </summary>
|
||||
string Hash { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ namespace osu.Game.Database
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
foreach (string newBeatmap in existing.BeatmapMD5Hashes)
|
||||
foreach (string newBeatmap in collection.BeatmapMD5Hashes)
|
||||
{
|
||||
if (!existing.BeatmapMD5Hashes.Contains(newBeatmap))
|
||||
existing.BeatmapMD5Hashes.Add(newBeatmap);
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
@ -27,27 +25,30 @@ namespace osu.Game.Database
|
||||
public class LegacyImportManager : Component
|
||||
{
|
||||
[Resolved]
|
||||
private SkinManager skins { get; set; }
|
||||
private SkinManager skins { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; }
|
||||
private BeatmapManager beatmaps { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scores { get; set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private OsuGame game { get; set; }
|
||||
private ScoreManager scores { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IDialogOverlay dialogOverlay { get; set; }
|
||||
private OsuGame? game { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realmAccess { get; set; }
|
||||
private IDialogOverlay dialogOverlay { get; set; } = null!;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private DesktopGameHost desktopGameHost { get; set; }
|
||||
[Resolved]
|
||||
private RealmAccess realmAccess { get; set; } = null!;
|
||||
|
||||
private StableStorage cachedStorage;
|
||||
[Resolved(canBeNull: true)] // canBeNull required while we remain on mono for mobile platforms.
|
||||
private DesktopGameHost? desktopGameHost { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private INotificationOverlay? notifications { get; set; }
|
||||
|
||||
private StableStorage? cachedStorage;
|
||||
|
||||
public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
|
||||
|
||||
@ -98,6 +99,9 @@ namespace osu.Game.Database
|
||||
stableStorage = GetCurrentStableStorage();
|
||||
}
|
||||
|
||||
if (stableStorage == null)
|
||||
return;
|
||||
|
||||
var importTasks = new List<Task>();
|
||||
|
||||
Task beatmapImportTask = Task.CompletedTask;
|
||||
@ -108,7 +112,14 @@ namespace osu.Game.Database
|
||||
importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage));
|
||||
|
||||
if (content.HasFlagFast(StableContent.Collections))
|
||||
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess).ImportFromStorage(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
|
||||
{
|
||||
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess)
|
||||
{
|
||||
// Other legacy importers import via model managers which handle the posting of notifications.
|
||||
// Collections are an exception.
|
||||
PostNotification = n => notifications?.Post(n)
|
||||
}.ImportFromStorage(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
|
||||
}
|
||||
|
||||
if (content.HasFlagFast(StableContent.Scores))
|
||||
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
|
||||
@ -116,7 +127,7 @@ namespace osu.Game.Database
|
||||
await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public StableStorage GetCurrentStableStorage()
|
||||
public StableStorage? GetCurrentStableStorage()
|
||||
{
|
||||
if (cachedStorage != null)
|
||||
return cachedStorage;
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Humanizer;
|
||||
using osu.Framework.Logging;
|
||||
@ -107,7 +108,15 @@ namespace osu.Game.Database
|
||||
notification.State = ProgressNotificationState.Cancelled;
|
||||
|
||||
if (!(error is OperationCanceledException))
|
||||
Logger.Error(error, $"{importer.HumanisedModelName.Titleize()} download failed!");
|
||||
{
|
||||
if (error is WebException webException && webException.Message == @"TooManyRequests")
|
||||
{
|
||||
notification.Close();
|
||||
PostNotification?.Invoke(new TooManyDownloadsNotification());
|
||||
}
|
||||
else
|
||||
Logger.Error(error, $"{importer.HumanisedModelName.Titleize()} download failed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
@ -79,7 +80,7 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
public void AddFile(TModel item, Stream contents, string filename, Realm realm)
|
||||
{
|
||||
var existing = item.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase));
|
||||
var existing = item.GetFile(filename);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
|
@ -25,6 +25,7 @@ using osu.Game.Configuration;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using Realms;
|
||||
@ -67,8 +68,10 @@ namespace osu.Game.Database
|
||||
/// 20 2022-07-21 Added LastAppliedDifficultyVersion to RulesetInfo, changed default value of BeatmapInfo.StarRating to -1.
|
||||
/// 21 2022-07-27 Migrate collections to realm (BeatmapCollection).
|
||||
/// 22 2022-07-31 Added ModPreset.
|
||||
/// 23 2022-08-01 Added LastLocalUpdate to BeatmapInfo.
|
||||
/// 24 2022-08-22 Added MaximumStatistics to ScoreInfo.
|
||||
/// </summary>
|
||||
private const int schema_version = 22;
|
||||
private const int schema_version = 24;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||
@ -172,6 +175,11 @@ namespace osu.Game.Database
|
||||
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
|
||||
Filename += realm_extension;
|
||||
|
||||
#if DEBUG
|
||||
if (!DebugUtils.IsNUnitRunning)
|
||||
applyFilenameSchemaSuffix(ref Filename);
|
||||
#endif
|
||||
|
||||
string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}";
|
||||
|
||||
// Attempt to recover a newer database version if available.
|
||||
@ -211,6 +219,51 @@ namespace osu.Game.Database
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Some developers may be annoyed if a newer version migration (ie. caused by testing a pull request)
|
||||
/// cause their test database to be unusable with previous versions.
|
||||
/// To get around this, store development databases against their realm version.
|
||||
/// Note that this means changes made on newer realm versions will disappear.
|
||||
/// </summary>
|
||||
private void applyFilenameSchemaSuffix(ref string filename)
|
||||
{
|
||||
string originalFilename = filename;
|
||||
|
||||
filename = getVersionedFilename(schema_version);
|
||||
|
||||
// First check if the current realm version already exists...
|
||||
if (storage.Exists(filename))
|
||||
return;
|
||||
|
||||
// Check for a previous version we can use as a base database to migrate from...
|
||||
for (int i = schema_version - 1; i >= 0; i--)
|
||||
{
|
||||
string previousFilename = getVersionedFilename(i);
|
||||
|
||||
if (storage.Exists(previousFilename))
|
||||
{
|
||||
copyPreviousVersion(previousFilename, filename);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, check for a non-versioned file exists (aka before this method was added)...
|
||||
if (storage.Exists(originalFilename))
|
||||
copyPreviousVersion(originalFilename, filename);
|
||||
|
||||
void copyPreviousVersion(string previousFilename, string newFilename)
|
||||
{
|
||||
using (var previous = storage.GetStream(previousFilename))
|
||||
using (var current = storage.CreateFileSafely(newFilename))
|
||||
{
|
||||
Logger.Log(@$"Copying previous realm database {previousFilename} to {newFilename} for migration to schema version {schema_version}");
|
||||
previous.CopyTo(current);
|
||||
}
|
||||
}
|
||||
|
||||
string getVersionedFilename(int version) => originalFilename.Replace(realm_extension, $"_{version}{realm_extension}");
|
||||
}
|
||||
|
||||
private void attemptRecoverFromFile(string recoveryFilename)
|
||||
{
|
||||
Logger.Log($@"Performing recovery from {recoveryFilename}", LoggingTarget.Database);
|
||||
@ -292,6 +345,11 @@ namespace osu.Game.Database
|
||||
foreach (var s in pendingDeleteSkins)
|
||||
realm.Remove(s);
|
||||
|
||||
var pendingDeletePresets = realm.All<ModPreset>().Where(s => s.DeletePending);
|
||||
|
||||
foreach (var s in pendingDeletePresets)
|
||||
realm.Remove(s);
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
@ -844,8 +902,15 @@ namespace osu.Game.Database
|
||||
try
|
||||
{
|
||||
using (var source = storage.GetStream(Filename, mode: FileMode.Open))
|
||||
using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
|
||||
source.CopyTo(destination);
|
||||
{
|
||||
// source may not exist.
|
||||
if (source == null)
|
||||
return;
|
||||
|
||||
using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
|
||||
source.CopyTo(destination);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
catch (IOException)
|
||||
|
26
osu.Game/Database/TooManyDownloadsNotification.cs
Normal file
26
osu.Game/Database/TooManyDownloadsNotification.cs
Normal file
@ -0,0 +1,26 @@
|
||||
// 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.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public class TooManyDownloadsNotification : SimpleNotification
|
||||
{
|
||||
public TooManyDownloadsNotification()
|
||||
{
|
||||
Text = BeatmapsetsStrings.DownloadLimitExceeded;
|
||||
Icon = FontAwesome.Solid.ExclamationCircle;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
IconBackground.Colour = colours.RedDark;
|
||||
}
|
||||
}
|
||||
}
|
@ -14,9 +14,8 @@ using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Allocation;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics.Batches;
|
||||
using osu.Framework.Graphics.OpenGL.Buffers;
|
||||
using osu.Framework.Graphics.OpenGL.Vertices;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Lists;
|
||||
|
||||
namespace osu.Game.Graphics.Backgrounds
|
||||
@ -88,7 +87,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
|
||||
private Random stableRandom;
|
||||
private IShader shader;
|
||||
private readonly Texture texture;
|
||||
private Texture texture;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a new triangle visualisation.
|
||||
@ -98,13 +97,12 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
{
|
||||
if (seed != null)
|
||||
stableRandom = new Random(seed.Value);
|
||||
|
||||
texture = Texture.WhitePixel;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ShaderManager shaders)
|
||||
private void load(IRenderer renderer, ShaderManager shaders)
|
||||
{
|
||||
texture = renderer.WhitePixel;
|
||||
shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
|
||||
}
|
||||
|
||||
@ -184,8 +182,8 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
|
||||
private void addTriangles(bool randomY)
|
||||
{
|
||||
// limited by the maximum size of QuadVertexBuffer for safety.
|
||||
const int max_triangles = QuadVertexBuffer<TexturedVertex2D>.MAX_QUADS;
|
||||
// Limited by the maximum size of QuadVertexBuffer for safety.
|
||||
const int max_triangles = ushort.MaxValue / (IRenderer.VERTICES_PER_QUAD + 2);
|
||||
|
||||
AimCount = (int)Math.Min(max_triangles, (DrawWidth * DrawHeight * 0.002f / (triangleScale * triangleScale) * SpawnRatio));
|
||||
|
||||
@ -251,7 +249,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
private readonly List<TriangleParticle> parts = new List<TriangleParticle>();
|
||||
private Vector2 size;
|
||||
|
||||
private QuadBatch<TexturedVertex2D> vertexBatch;
|
||||
private IVertexBatch<TexturedVertex2D> vertexBatch;
|
||||
|
||||
public TrianglesDrawNode(Triangles source)
|
||||
: base(source)
|
||||
@ -270,14 +268,14 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
parts.AddRange(Source.parts);
|
||||
}
|
||||
|
||||
public override void Draw(Action<TexturedVertex2D> vertexAction)
|
||||
public override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(vertexAction);
|
||||
base.Draw(renderer);
|
||||
|
||||
if (Source.AimCount > 0 && (vertexBatch == null || vertexBatch.Size != Source.AimCount))
|
||||
{
|
||||
vertexBatch?.Dispose();
|
||||
vertexBatch = new QuadBatch<TexturedVertex2D>(Source.AimCount, 1);
|
||||
vertexBatch = renderer.CreateQuadBatch<TexturedVertex2D>(Source.AimCount, 1);
|
||||
}
|
||||
|
||||
shader.Bind();
|
||||
@ -297,7 +295,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
ColourInfo colourInfo = DrawColourInfo.Colour;
|
||||
colourInfo.ApplyChild(particle.Colour);
|
||||
|
||||
DrawTriangle(
|
||||
renderer.DrawTriangle(
|
||||
texture,
|
||||
triangle,
|
||||
colourInfo,
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
@ -10,13 +8,12 @@ using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
/// <summary>
|
||||
/// A container which fires a callback when a new beat is reached.
|
||||
/// Consumes a parent <see cref="GameplayClock"/> or <see cref="Beatmap"/> (whichever is first available).
|
||||
/// Consumes a parent <see cref="IBeatSyncProvider"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This container does not set its own clock to the source used for beat matching.
|
||||
@ -28,8 +25,10 @@ namespace osu.Game.Graphics.Containers
|
||||
public class BeatSyncedContainer : Container
|
||||
{
|
||||
private int lastBeat;
|
||||
protected TimingControlPoint LastTimingPoint { get; private set; }
|
||||
protected EffectControlPoint LastEffectPoint { get; private set; }
|
||||
|
||||
private TimingControlPoint? lastTimingPoint { get; set; }
|
||||
|
||||
protected bool IsKiaiTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of time before a beat we should fire <see cref="OnNewBeat(int, TimingControlPoint, EffectControlPoint, ChannelAmplitudes)"/>.
|
||||
@ -71,12 +70,12 @@ namespace osu.Game.Graphics.Containers
|
||||
public double MinimumBeatLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this container is currently tracking a beatmap's timing data.
|
||||
/// Whether this container is currently tracking a beat sync provider.
|
||||
/// </summary>
|
||||
protected bool IsBeatSyncedWithTrack { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
protected IBeatSyncProvider BeatSyncSource { get; private set; }
|
||||
protected IBeatSyncProvider BeatSyncSource { get; private set; } = null!;
|
||||
|
||||
protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
|
||||
{
|
||||
@ -87,19 +86,18 @@ namespace osu.Game.Graphics.Containers
|
||||
TimingControlPoint timingPoint;
|
||||
EffectControlPoint effectPoint;
|
||||
|
||||
IsBeatSyncedWithTrack = BeatSyncSource.Clock?.IsRunning == true && BeatSyncSource.ControlPoints != null;
|
||||
IsBeatSyncedWithTrack = BeatSyncSource.CheckBeatSyncAvailable() && BeatSyncSource.Clock?.IsRunning == true;
|
||||
|
||||
double currentTrackTime;
|
||||
|
||||
if (IsBeatSyncedWithTrack)
|
||||
{
|
||||
Debug.Assert(BeatSyncSource.ControlPoints != null);
|
||||
Debug.Assert(BeatSyncSource.Clock != null);
|
||||
|
||||
currentTrackTime = BeatSyncSource.Clock.CurrentTime + EarlyActivationMilliseconds;
|
||||
|
||||
timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(currentTrackTime);
|
||||
effectPoint = BeatSyncSource.ControlPoints.EffectPointAt(currentTrackTime);
|
||||
timingPoint = BeatSyncSource.ControlPoints?.TimingPointAt(currentTrackTime) ?? TimingControlPoint.DEFAULT;
|
||||
effectPoint = BeatSyncSource.ControlPoints?.EffectPointAt(currentTrackTime) ?? EffectControlPoint.DEFAULT;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -128,7 +126,7 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
TimeSinceLastBeat = beatLength - TimeUntilNextBeat;
|
||||
|
||||
if (ReferenceEquals(timingPoint, LastTimingPoint) && beatIndex == lastBeat)
|
||||
if (ReferenceEquals(timingPoint, lastTimingPoint) && beatIndex == lastBeat)
|
||||
return;
|
||||
|
||||
// as this event is sometimes used for sound triggers where `BeginDelayedSequence` has no effect, avoid firing it if too far away from the beat.
|
||||
@ -136,12 +134,13 @@ namespace osu.Game.Graphics.Containers
|
||||
if (AllowMistimedEventFiring || Math.Abs(TimeSinceLastBeat) < MISTIMED_ALLOWANCE)
|
||||
{
|
||||
using (BeginDelayedSequence(-TimeSinceLastBeat))
|
||||
OnNewBeat(beatIndex, timingPoint, effectPoint, BeatSyncSource.Amplitudes ?? ChannelAmplitudes.Empty);
|
||||
OnNewBeat(beatIndex, timingPoint, effectPoint, BeatSyncSource.CurrentAmplitudes);
|
||||
}
|
||||
|
||||
lastBeat = beatIndex;
|
||||
LastTimingPoint = timingPoint;
|
||||
LastEffectPoint = effectPoint;
|
||||
lastTimingPoint = timingPoint;
|
||||
|
||||
IsKiaiTime = effectPoint.KiaiMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using osu.Framework.Graphics.OpenGL.Vertices;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Graphics.ES30;
|
||||
|
@ -137,6 +137,9 @@ namespace osu.Game.Graphics
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case BeatmapOnlineStatus.LocallyModified:
|
||||
return Color4.OrangeRed;
|
||||
|
||||
case BeatmapOnlineStatus.Ranked:
|
||||
case BeatmapOnlineStatus.Approved:
|
||||
return Color4Extensions.FromHex(@"b3ff66");
|
||||
|
@ -6,8 +6,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.OpenGL.Vertices;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Utils;
|
||||
@ -89,7 +89,7 @@ namespace osu.Game.Graphics
|
||||
currentTime = source.Time.Current;
|
||||
}
|
||||
|
||||
protected override void Blit(Action<TexturedVertex2D> vertexAction)
|
||||
protected override void Blit(IRenderer renderer)
|
||||
{
|
||||
double time = currentTime - startTime;
|
||||
|
||||
@ -112,9 +112,9 @@ namespace osu.Game.Graphics
|
||||
Vector2Extensions.Transform(rect.BottomRight, DrawInfo.Matrix)
|
||||
);
|
||||
|
||||
DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), null, vertexAction,
|
||||
new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height),
|
||||
null, TextureCoords);
|
||||
renderer.DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha),
|
||||
inflationPercentage: new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height),
|
||||
textureCoords: TextureCoords);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,8 +7,8 @@ using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.OpenGL.Vertices;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Utils;
|
||||
@ -107,7 +107,7 @@ namespace osu.Game.Graphics
|
||||
sourceSize = Source.DrawSize;
|
||||
}
|
||||
|
||||
protected override void Blit(Action<TexturedVertex2D> vertexAction)
|
||||
protected override void Blit(IRenderer renderer)
|
||||
{
|
||||
foreach (var p in particles)
|
||||
{
|
||||
@ -136,9 +136,9 @@ namespace osu.Game.Graphics
|
||||
transformPosition(rect.BottomRight, rect.Centre, angle)
|
||||
);
|
||||
|
||||
DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), null, vertexAction,
|
||||
new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height),
|
||||
null, TextureCoords);
|
||||
renderer.DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha),
|
||||
inflationPercentage: new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height),
|
||||
textureCoords: TextureCoords);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,10 +3,9 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.OpenGL.Vertices;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
|
||||
@ -57,11 +56,11 @@ namespace osu.Game.Graphics.Sprites
|
||||
progress = source.animationProgress;
|
||||
}
|
||||
|
||||
protected override void Blit(Action<TexturedVertex2D> vertexAction)
|
||||
protected override void Blit(IRenderer renderer)
|
||||
{
|
||||
Shader.GetUniform<float>("progress").UpdateValue(ref progress);
|
||||
GetAppropriateShader(renderer).GetUniform<float>("progress").UpdateValue(ref progress);
|
||||
|
||||
base.Blit(vertexAction);
|
||||
base.Blit(renderer);
|
||||
}
|
||||
|
||||
protected override bool CanDrawOpaqueInterior => false;
|
||||
|
@ -167,6 +167,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// If the game goes into a suspended state (ie. debugger attached or backgrounded on a mobile device)
|
||||
// we want to ignore really long periods of no processing.
|
||||
if (updateClock.ElapsedFrameTime > 10000)
|
||||
return;
|
||||
|
||||
mainContent.Width = Math.Max(mainContent.Width, counters.DrawWidth);
|
||||
|
||||
// Handle the case where the window has become inactive or the user changed the
|
||||
@ -177,15 +182,15 @@ namespace osu.Game.Graphics.UserInterface
|
||||
// use elapsed frame time rather then FramesPerSecond to better catch stutter frames.
|
||||
bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && drawClock.ElapsedFrameTime > spike_time_ms;
|
||||
|
||||
// note that we use an elapsed time here of 1 intentionally.
|
||||
// this weights all updates equally. if we passed in the elapsed time, longer frames would be weighted incorrectly lower.
|
||||
displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, updateClock.ElapsedFrameTime, hasUpdateSpike ? 0 : 100, 1);
|
||||
const float damp_time = 100;
|
||||
|
||||
displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, updateClock.ElapsedFrameTime, hasUpdateSpike ? 0 : damp_time, updateClock.ElapsedFrameTime);
|
||||
|
||||
if (hasDrawSpike)
|
||||
// show spike time using raw elapsed value, to account for `FramesPerSecond` being so averaged spike frames don't show.
|
||||
displayedFpsCount = 1000 / drawClock.ElapsedFrameTime;
|
||||
else
|
||||
displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, 100, Time.Elapsed);
|
||||
displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, damp_time, Time.Elapsed);
|
||||
|
||||
if (Time.Current - lastUpdate > min_time_between_updates)
|
||||
{
|
||||
@ -203,7 +208,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
if (hasSignificantChanges)
|
||||
requestDisplay();
|
||||
else if (isDisplayed && Time.Current - lastDisplayRequiredTime > 2000)
|
||||
else if (isDisplayed && Time.Current - lastDisplayRequiredTime > 2000 && !IsHovered)
|
||||
{
|
||||
mainContent.FadeTo(0, 300, Easing.OutQuint);
|
||||
isDisplayed = false;
|
||||
|
@ -20,6 +20,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// </summary>
|
||||
public class LoadingLayer : LoadingSpinner
|
||||
{
|
||||
private readonly bool blockInput;
|
||||
|
||||
[CanBeNull]
|
||||
protected Box BackgroundDimLayer { get; }
|
||||
|
||||
@ -28,9 +30,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// </summary>
|
||||
/// <param name="dimBackground">Whether the full background area should be dimmed while loading.</param>
|
||||
/// <param name="withBox">Whether the spinner should have a surrounding black box for visibility.</param>
|
||||
public LoadingLayer(bool dimBackground = false, bool withBox = true)
|
||||
/// <param name="blockInput">Whether to block input of components behind the loading layer.</param>
|
||||
public LoadingLayer(bool dimBackground = false, bool withBox = true, bool blockInput = true)
|
||||
: base(withBox)
|
||||
{
|
||||
this.blockInput = blockInput;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Size = new Vector2(1);
|
||||
|
||||
@ -52,6 +56,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
protected override bool Handle(UIEvent e)
|
||||
{
|
||||
if (!blockInput)
|
||||
return false;
|
||||
|
||||
switch (e)
|
||||
{
|
||||
// blocking scroll can cause weird behaviour when this layer is used within a ScrollContainer.
|
||||
@ -83,7 +90,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
base.Update();
|
||||
|
||||
MainContents.Size = new Vector2(Math.Clamp(Math.Min(DrawWidth, DrawHeight) * 0.25f, 30, 100));
|
||||
MainContents.Size = new Vector2(Math.Clamp(Math.Min(DrawWidth, DrawHeight) * 0.25f, 20, 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,15 +49,15 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Active.BindDisabledChanged(disabled => Action = disabled ? null : Active.Toggle, true);
|
||||
Active.BindValueChanged(_ =>
|
||||
{
|
||||
updateActiveState();
|
||||
UpdateActiveState();
|
||||
playSample();
|
||||
});
|
||||
|
||||
updateActiveState();
|
||||
UpdateActiveState();
|
||||
base.LoadComplete();
|
||||
}
|
||||
|
||||
private void updateActiveState()
|
||||
protected virtual void UpdateActiveState()
|
||||
{
|
||||
DarkerColour = Active.Value ? ColourProvider.Highlight1 : ColourProvider.Background3;
|
||||
LighterColour = Active.Value ? ColourProvider.Colour0 : ColourProvider.Background1;
|
||||
|
@ -13,6 +13,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
@ -26,9 +27,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
public BindableList<Colour4> Colours { get; } = new BindableList<Colour4>();
|
||||
|
||||
private string colourNamePrefix = "Colour";
|
||||
private LocalisableString colourNamePrefix = "Colour";
|
||||
|
||||
public string ColourNamePrefix
|
||||
public LocalisableString ColourNamePrefix
|
||||
{
|
||||
get => colourNamePrefix;
|
||||
set
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
@ -17,7 +18,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
public BindableList<Colour4> Colours => Component.Colours;
|
||||
|
||||
public string ColourNamePrefix
|
||||
public LocalisableString ColourNamePrefix
|
||||
{
|
||||
get => Component.ColourNamePrefix;
|
||||
set => Component.ColourNamePrefix = value;
|
||||
|
@ -1,9 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Game.Database;
|
||||
@ -12,10 +11,15 @@ namespace osu.Game.IO
|
||||
{
|
||||
public interface IStorageResourceProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// The game renderer.
|
||||
/// </summary>
|
||||
IRenderer Renderer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the game-wide audio manager.
|
||||
/// </summary>
|
||||
AudioManager AudioManager { get; }
|
||||
AudioManager? AudioManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Access game-wide user files.
|
||||
|
@ -26,6 +26,11 @@ namespace osu.Game.IO
|
||||
/// </summary>
|
||||
public virtual string[] IgnoreFiles => Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// A list of file/directory suffixes which should not be migrated.
|
||||
/// </summary>
|
||||
public virtual string[] IgnoreSuffixes => Array.Empty<string>();
|
||||
|
||||
protected MigratableStorage(Storage storage, string subPath = null)
|
||||
: base(storage, subPath)
|
||||
{
|
||||
@ -73,6 +78,9 @@ namespace osu.Game.IO
|
||||
if (topLevelExcludes && IgnoreFiles.Contains(fi.Name))
|
||||
continue;
|
||||
|
||||
if (IgnoreSuffixes.Any(suffix => fi.Name.EndsWith(suffix, StringComparison.Ordinal)))
|
||||
continue;
|
||||
|
||||
allFilesDeleted &= AttemptOperation(() => fi.Delete(), throwOnFailure: false);
|
||||
}
|
||||
|
||||
@ -81,6 +89,9 @@ namespace osu.Game.IO
|
||||
if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name))
|
||||
continue;
|
||||
|
||||
if (IgnoreSuffixes.Any(suffix => dir.Name.EndsWith(suffix, StringComparison.Ordinal)))
|
||||
continue;
|
||||
|
||||
allFilesDeleted &= AttemptOperation(() => dir.Delete(true), throwOnFailure: false);
|
||||
}
|
||||
|
||||
@ -101,6 +112,9 @@ namespace osu.Game.IO
|
||||
if (topLevelExcludes && IgnoreFiles.Contains(fileInfo.Name))
|
||||
continue;
|
||||
|
||||
if (IgnoreSuffixes.Any(suffix => fileInfo.Name.EndsWith(suffix, StringComparison.Ordinal)))
|
||||
continue;
|
||||
|
||||
AttemptOperation(() =>
|
||||
{
|
||||
fileInfo.Refresh();
|
||||
@ -119,6 +133,9 @@ namespace osu.Game.IO
|
||||
if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name))
|
||||
continue;
|
||||
|
||||
if (IgnoreSuffixes.Any(suffix => dir.Name.EndsWith(suffix, StringComparison.Ordinal)))
|
||||
continue;
|
||||
|
||||
CopyRecursive(dir, destination.CreateSubdirectory(dir.Name), false);
|
||||
}
|
||||
}
|
||||
|
@ -38,15 +38,20 @@ namespace osu.Game.IO
|
||||
public override string[] IgnoreDirectories => new[]
|
||||
{
|
||||
"cache",
|
||||
$"{OsuGameBase.CLIENT_DATABASE_FILENAME}.management",
|
||||
};
|
||||
|
||||
public override string[] IgnoreFiles => new[]
|
||||
{
|
||||
"framework.ini",
|
||||
"storage.ini",
|
||||
$"{OsuGameBase.CLIENT_DATABASE_FILENAME}.note",
|
||||
$"{OsuGameBase.CLIENT_DATABASE_FILENAME}.lock",
|
||||
};
|
||||
|
||||
public override string[] IgnoreSuffixes => new[]
|
||||
{
|
||||
// Realm pipe files don't play well with copy operations
|
||||
".note",
|
||||
".lock",
|
||||
".management",
|
||||
};
|
||||
|
||||
public OsuStorage(GameHost host, Storage defaultStorage)
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
@ -15,10 +13,11 @@ namespace osu.Game.Input.Bindings
|
||||
{
|
||||
public class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput
|
||||
{
|
||||
private readonly Drawable handler;
|
||||
private InputManager parentInputManager;
|
||||
private readonly Drawable? handler;
|
||||
|
||||
public GlobalActionContainer(OsuGameBase game)
|
||||
private InputManager? parentInputManager;
|
||||
|
||||
public GlobalActionContainer(OsuGameBase? game)
|
||||
: base(matchingMode: KeyCombinationMatchingMode.Modifiers)
|
||||
{
|
||||
if (game is IKeyBindingHandler<GlobalAction>)
|
||||
@ -32,7 +31,10 @@ namespace osu.Game.Input.Bindings
|
||||
parentInputManager = GetContainingInputManager();
|
||||
}
|
||||
|
||||
// IMPORTANT: Do not change the order of key bindings in this list.
|
||||
// It is used to decide the order of precedence (see note in DatabasedKeyBindingContainer).
|
||||
public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings
|
||||
.Concat(OverlayKeyBindings)
|
||||
.Concat(EditorKeyBindings)
|
||||
.Concat(InGameKeyBindings)
|
||||
.Concat(SongSelectKeyBindings)
|
||||
@ -40,25 +42,6 @@ namespace osu.Game.Input.Bindings
|
||||
|
||||
public IEnumerable<KeyBinding> GlobalKeyBindings => new[]
|
||||
{
|
||||
new KeyBinding(InputKey.F6, GlobalAction.ToggleNowPlaying),
|
||||
new KeyBinding(InputKey.F8, GlobalAction.ToggleChat),
|
||||
new KeyBinding(InputKey.F9, GlobalAction.ToggleSocial),
|
||||
new KeyBinding(InputKey.F10, GlobalAction.ToggleGameplayMouseButtons),
|
||||
new KeyBinding(InputKey.F12, GlobalAction.TakeScreenshot),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay),
|
||||
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor),
|
||||
|
||||
new KeyBinding(InputKey.Escape, GlobalAction.Back),
|
||||
new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back),
|
||||
|
||||
new KeyBinding(new[] { InputKey.Alt, InputKey.Home }, GlobalAction.Home),
|
||||
|
||||
new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious),
|
||||
new KeyBinding(InputKey.Down, GlobalAction.SelectNext),
|
||||
|
||||
@ -69,7 +52,32 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(InputKey.Enter, GlobalAction.Select),
|
||||
new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select),
|
||||
|
||||
new KeyBinding(InputKey.Escape, GlobalAction.Back),
|
||||
new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back),
|
||||
|
||||
new KeyBinding(new[] { InputKey.Alt, InputKey.Home }, GlobalAction.Home),
|
||||
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ToggleProfile),
|
||||
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings),
|
||||
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.R }, GlobalAction.RandomSkin),
|
||||
|
||||
new KeyBinding(InputKey.F10, GlobalAction.ToggleGameplayMouseButtons),
|
||||
new KeyBinding(InputKey.F12, GlobalAction.TakeScreenshot),
|
||||
};
|
||||
|
||||
public IEnumerable<KeyBinding> OverlayKeyBindings => new[]
|
||||
{
|
||||
new KeyBinding(InputKey.F8, GlobalAction.ToggleChat),
|
||||
new KeyBinding(InputKey.F6, GlobalAction.ToggleNowPlaying),
|
||||
new KeyBinding(InputKey.F9, GlobalAction.ToggleSocial),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
|
||||
};
|
||||
|
||||
public IEnumerable<KeyBinding> EditorKeyBindings => new[]
|
||||
@ -332,5 +340,8 @@ namespace osu.Game.Input.Bindings
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleFPSCounter))]
|
||||
ToggleFPSDisplay,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleProfile))]
|
||||
ToggleProfile,
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,9 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Input
|
||||
{
|
||||
@ -17,18 +18,20 @@ namespace osu.Game.Input
|
||||
/// <summary>
|
||||
/// The mouse cursor will be free to move outside the game window.
|
||||
/// </summary>
|
||||
[LocalisableDescription(typeof(MouseSettingsStrings), nameof(MouseSettingsStrings.NeverConfine))]
|
||||
Never,
|
||||
|
||||
/// <summary>
|
||||
/// The mouse cursor will be locked to the window bounds during gameplay,
|
||||
/// but may otherwise move freely.
|
||||
/// </summary>
|
||||
[Description("During Gameplay")]
|
||||
[LocalisableDescription(typeof(MouseSettingsStrings), nameof(MouseSettingsStrings.ConfineDuringGameplay))]
|
||||
DuringGameplay,
|
||||
|
||||
/// <summary>
|
||||
/// The mouse cursor will always be locked to the window bounds while the game has focus.
|
||||
/// </summary>
|
||||
[LocalisableDescription(typeof(MouseSettingsStrings), nameof(MouseSettingsStrings.AlwaysConfine))]
|
||||
Always
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
@ -89,6 +89,16 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString Collections => new TranslatableString(getKey(@"collections"), @"Collections");
|
||||
|
||||
/// <summary>
|
||||
/// "Name"
|
||||
/// </summary>
|
||||
public static LocalisableString Name => new TranslatableString(getKey(@"name"), @"Name");
|
||||
|
||||
/// <summary>
|
||||
/// "Description"
|
||||
/// </summary>
|
||||
public static LocalisableString Description => new TranslatableString(getKey(@"description"), @"Description");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
29
osu.Game/Localisation/DeleteConfirmationDialogStrings.cs
Normal file
29
osu.Game/Localisation/DeleteConfirmationDialogStrings.cs
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class DeleteConfirmationDialogStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.DeleteConfirmationDialog";
|
||||
|
||||
/// <summary>
|
||||
/// "Confirm deletion of"
|
||||
/// </summary>
|
||||
public static LocalisableString HeaderText => new TranslatableString(getKey(@"header_text"), @"Confirm deletion of");
|
||||
|
||||
/// <summary>
|
||||
/// "Yes. Go for it."
|
||||
/// </summary>
|
||||
public static LocalisableString Confirm => new TranslatableString(getKey(@"confirm"), @"Yes. Go for it.");
|
||||
|
||||
/// <summary>
|
||||
/// "No! Abort mission"
|
||||
/// </summary>
|
||||
public static LocalisableString Cancel => new TranslatableString(getKey(@"cancel"), @"No! Abort mission");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
204
osu.Game/Localisation/EditorSetupStrings.cs
Normal file
204
osu.Game/Localisation/EditorSetupStrings.cs
Normal file
@ -0,0 +1,204 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class EditorSetupStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.EditorSetup";
|
||||
|
||||
/// <summary>
|
||||
/// "Beatmap Setup"
|
||||
/// </summary>
|
||||
public static LocalisableString BeatmapSetup => new TranslatableString(getKey(@"beatmap_setup"), @"Beatmap Setup");
|
||||
|
||||
/// <summary>
|
||||
/// "change general settings of your beatmap"
|
||||
/// </summary>
|
||||
public static LocalisableString BeatmapSetupDescription => new TranslatableString(getKey(@"beatmap_setup_description"), @"change general settings of your beatmap");
|
||||
|
||||
/// <summary>
|
||||
/// "Colours"
|
||||
/// </summary>
|
||||
public static LocalisableString ColoursHeader => new TranslatableString(getKey(@"colours_header"), @"Colours");
|
||||
|
||||
/// <summary>
|
||||
/// "Hit circle / Slider Combos"
|
||||
/// </summary>
|
||||
public static LocalisableString HitCircleSliderCombos => new TranslatableString(getKey(@"hit_circle_slider_combos"), @"Hit circle / Slider Combos");
|
||||
|
||||
/// <summary>
|
||||
/// "Design"
|
||||
/// </summary>
|
||||
public static LocalisableString DesignHeader => new TranslatableString(getKey(@"design_header"), @"Design");
|
||||
|
||||
/// <summary>
|
||||
/// "Enable countdown"
|
||||
/// </summary>
|
||||
public static LocalisableString EnableCountdown => new TranslatableString(getKey(@"enable_countdown"), @"Enable countdown");
|
||||
|
||||
/// <summary>
|
||||
/// "If enabled, an "Are you ready? 3, 2, 1, GO!" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so."
|
||||
/// </summary>
|
||||
public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"), @"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so.");
|
||||
|
||||
/// <summary>
|
||||
/// "Countdown speed"
|
||||
/// </summary>
|
||||
public static LocalisableString CountdownSpeed => new TranslatableString(getKey(@"countdown_speed"), @"Countdown speed");
|
||||
|
||||
/// <summary>
|
||||
/// "If the countdown sounds off-time, use this to make it appear one or more beats early."
|
||||
/// </summary>
|
||||
public static LocalisableString CountdownOffsetDescription => new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early.");
|
||||
|
||||
/// <summary>
|
||||
/// "Countdown offset"
|
||||
/// </summary>
|
||||
public static LocalisableString CountdownOffset => new TranslatableString(getKey(@"countdown_offset"), @"Countdown offset");
|
||||
|
||||
/// <summary>
|
||||
/// "Widescreen support"
|
||||
/// </summary>
|
||||
public static LocalisableString WidescreenSupport => new TranslatableString(getKey(@"widescreen_support"), @"Widescreen support");
|
||||
|
||||
/// <summary>
|
||||
/// "Allows storyboards to use the full screen space, rather than be confined to a 4:3 area."
|
||||
/// </summary>
|
||||
public static LocalisableString WidescreenSupportDescription => new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area.");
|
||||
|
||||
/// <summary>
|
||||
/// "Epilepsy warning"
|
||||
/// </summary>
|
||||
public static LocalisableString EpilepsyWarning => new TranslatableString(getKey(@"epilepsy_warning"), @"Epilepsy warning");
|
||||
|
||||
/// <summary>
|
||||
/// "Recommended if the storyboard or video contain scenes with rapidly flashing colours."
|
||||
/// </summary>
|
||||
public static LocalisableString EpilepsyWarningDescription => new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours.");
|
||||
|
||||
/// <summary>
|
||||
/// "Letterbox during breaks"
|
||||
/// </summary>
|
||||
public static LocalisableString LetterboxDuringBreaks => new TranslatableString(getKey(@"letterbox_during_breaks"), @"Letterbox during breaks");
|
||||
|
||||
/// <summary>
|
||||
/// "Adds horizontal letterboxing to give a cinematic look during breaks."
|
||||
/// </summary>
|
||||
public static LocalisableString LetterboxDuringBreaksDescription => new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks.");
|
||||
|
||||
/// <summary>
|
||||
/// "Samples match playback rate"
|
||||
/// </summary>
|
||||
public static LocalisableString SamplesMatchPlaybackRate => new TranslatableString(getKey(@"samples_match_playback_rate"), @"Samples match playback rate");
|
||||
|
||||
/// <summary>
|
||||
/// "When enabled, all samples will speed up or slow down when rate-changing mods are enabled."
|
||||
/// </summary>
|
||||
public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"), @"When enabled, all samples will speed up or slow down when rate-changing mods are enabled.");
|
||||
|
||||
/// <summary>
|
||||
/// "The size of all hit objects"
|
||||
/// </summary>
|
||||
public static LocalisableString CircleSizeDescription => new TranslatableString(getKey(@"circle_size_description"), @"The size of all hit objects");
|
||||
|
||||
/// <summary>
|
||||
/// "The rate of passive health drain throughout playable time"
|
||||
/// </summary>
|
||||
public static LocalisableString DrainRateDescription => new TranslatableString(getKey(@"drain_rate_description"), @"The rate of passive health drain throughout playable time");
|
||||
|
||||
/// <summary>
|
||||
/// "The speed at which objects are presented to the player"
|
||||
/// </summary>
|
||||
public static LocalisableString ApproachRateDescription => new TranslatableString(getKey(@"approach_rate_description"), @"The speed at which objects are presented to the player");
|
||||
|
||||
/// <summary>
|
||||
/// "The harshness of hit windows and difficulty of special objects (ie. spinners)"
|
||||
/// </summary>
|
||||
public static LocalisableString OverallDifficultyDescription => new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)");
|
||||
|
||||
/// <summary>
|
||||
/// "Metadata"
|
||||
/// </summary>
|
||||
public static LocalisableString MetadataHeader => new TranslatableString(getKey(@"metadata_header"), @"Metadata");
|
||||
|
||||
/// <summary>
|
||||
/// "Romanised Artist"
|
||||
/// </summary>
|
||||
public static LocalisableString RomanisedArtist => new TranslatableString(getKey(@"romanised_artist"), @"Romanised Artist");
|
||||
|
||||
/// <summary>
|
||||
/// "Romanised Title"
|
||||
/// </summary>
|
||||
public static LocalisableString RomanisedTitle => new TranslatableString(getKey(@"romanised_title"), @"Romanised Title");
|
||||
|
||||
/// <summary>
|
||||
/// "Creator"
|
||||
/// </summary>
|
||||
public static LocalisableString Creator => new TranslatableString(getKey(@"creator"), @"Creator");
|
||||
|
||||
/// <summary>
|
||||
/// "Difficulty Name"
|
||||
/// </summary>
|
||||
public static LocalisableString DifficultyName => new TranslatableString(getKey(@"difficulty_name"), @"Difficulty Name");
|
||||
|
||||
/// <summary>
|
||||
/// "Resources"
|
||||
/// </summary>
|
||||
public static LocalisableString ResourcesHeader => new TranslatableString(getKey(@"resources_header"), @"Resources");
|
||||
|
||||
/// <summary>
|
||||
/// "Audio Track"
|
||||
/// </summary>
|
||||
public static LocalisableString AudioTrack => new TranslatableString(getKey(@"audio_track"), @"Audio Track");
|
||||
|
||||
/// <summary>
|
||||
/// "Click to select a track"
|
||||
/// </summary>
|
||||
public static LocalisableString ClickToSelectTrack => new TranslatableString(getKey(@"click_to_select_track"), @"Click to select a track");
|
||||
|
||||
/// <summary>
|
||||
/// "Click to replace the track"
|
||||
/// </summary>
|
||||
public static LocalisableString ClickToReplaceTrack => new TranslatableString(getKey(@"click_to_replace_track"), @"Click to replace the track");
|
||||
|
||||
/// <summary>
|
||||
/// "Click to select a background image"
|
||||
/// </summary>
|
||||
public static LocalisableString ClickToSelectBackground => new TranslatableString(getKey(@"click_to_select_background"), @"Click to select a background image");
|
||||
|
||||
/// <summary>
|
||||
/// "Click to replace the background image"
|
||||
/// </summary>
|
||||
public static LocalisableString ClickToReplaceBackground => new TranslatableString(getKey(@"click_to_replace_background"), @"Click to replace the background image");
|
||||
|
||||
/// <summary>
|
||||
/// "Ruleset ({0})"
|
||||
/// </summary>
|
||||
public static LocalisableString RulesetHeader(string arg0) => new TranslatableString(getKey(@"ruleset"), @"Ruleset ({0})", arg0);
|
||||
|
||||
/// <summary>
|
||||
/// "Combo"
|
||||
/// </summary>
|
||||
public static LocalisableString ComboColourPrefix => new TranslatableString(getKey(@"combo_colour_prefix"), @"Combo");
|
||||
|
||||
/// <summary>
|
||||
/// "Artist"
|
||||
/// </summary>
|
||||
public static LocalisableString Artist => new TranslatableString(getKey(@"artist"), @"Artist");
|
||||
|
||||
/// <summary>
|
||||
/// "Title"
|
||||
/// </summary>
|
||||
public static LocalisableString Title => new TranslatableString(getKey(@"title"), @"Title");
|
||||
|
||||
/// <summary>
|
||||
/// "Difficulty"
|
||||
/// </summary>
|
||||
public static LocalisableString DifficultyHeader => new TranslatableString(getKey(@"difficulty_header"), @"Difficulty");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
@ -104,6 +104,31 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString IncreaseFirstObjectVisibility => new TranslatableString(getKey(@"increase_first_object_visibility"), @"Increase visibility of first object when visual impairment mods are enabled");
|
||||
|
||||
/// <summary>
|
||||
/// "Hide during gameplay"
|
||||
/// </summary>
|
||||
public static LocalisableString HideDuringGameplay => new TranslatableString(getKey(@"hide_during_gameplay"), @"Hide during gameplay");
|
||||
|
||||
/// <summary>
|
||||
/// "Always"
|
||||
/// </summary>
|
||||
public static LocalisableString AlwaysShowHUD => new TranslatableString(getKey(@"always_show_hud"), @"Always");
|
||||
|
||||
/// <summary>
|
||||
/// "Never"
|
||||
/// </summary>
|
||||
public static LocalisableString NeverShowHUD => new TranslatableString(getKey(@"never_show_hud"), @"Never");
|
||||
|
||||
/// <summary>
|
||||
/// "Standardised"
|
||||
/// </summary>
|
||||
public static LocalisableString StandardisedScoreDisplay => new TranslatableString(getKey(@"standardised_score_display"), @"Standardised");
|
||||
|
||||
/// <summary>
|
||||
/// "Classic"
|
||||
/// </summary>
|
||||
public static LocalisableString ClassicScoreDisplay => new TranslatableString(getKey(@"classic_score_display"), @"Classic");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -149,6 +149,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ToggleNotifications => new TranslatableString(getKey(@"toggle_notifications"), @"Toggle notifications");
|
||||
|
||||
/// <summary>
|
||||
/// "Toggle profile"
|
||||
/// </summary>
|
||||
public static LocalisableString ToggleProfile => new TranslatableString(getKey(@"toggle_profile"), @"Toggle profile");
|
||||
|
||||
/// <summary>
|
||||
/// "Pause gameplay"
|
||||
/// </summary>
|
||||
|
@ -129,6 +129,16 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString UseHardwareAcceleration => new TranslatableString(getKey(@"use_hardware_acceleration"), @"Use hardware acceleration");
|
||||
|
||||
/// <summary>
|
||||
/// "JPG (web-friendly)"
|
||||
/// </summary>
|
||||
public static LocalisableString Jpg => new TranslatableString(getKey(@"jpg_web_friendly"), @"JPG (web-friendly)");
|
||||
|
||||
/// <summary>
|
||||
/// "PNG (lossless)"
|
||||
/// </summary>
|
||||
public static LocalisableString Png => new TranslatableString(getKey(@"png_lossless"), @"PNG (lossless)");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString GlobalKeyBindingHeader => new TranslatableString(getKey(@"global_key_binding_header"), @"Global");
|
||||
|
||||
/// <summary>
|
||||
/// "Overlays"
|
||||
/// </summary>
|
||||
public static LocalisableString OverlaysSection => new TranslatableString(getKey(@"overlays_section"), @"Overlays");
|
||||
|
||||
/// <summary>
|
||||
/// "Song Select"
|
||||
/// </summary>
|
||||
|
@ -29,6 +29,26 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString FullscreenMacOSNote => new TranslatableString(getKey(@"fullscreen_macos_note"), @"Using fullscreen on macOS makes interacting with the menu bar and spaces no longer work, and may lead to freezes if a system dialog is presented. Using borderless is recommended.");
|
||||
|
||||
/// <summary>
|
||||
/// "Excluding overlays"
|
||||
/// </summary>
|
||||
public static LocalisableString ScaleEverythingExcludingOverlays => new TranslatableString(getKey(@"scale_everything_excluding_overlays"), @"Excluding overlays");
|
||||
|
||||
/// <summary>
|
||||
/// "Everything"
|
||||
/// </summary>
|
||||
public static LocalisableString ScaleEverything => new TranslatableString(getKey(@"scale_everything"), @"Everything");
|
||||
|
||||
/// <summary>
|
||||
/// "Gameplay"
|
||||
/// </summary>
|
||||
public static LocalisableString ScaleGameplay => new TranslatableString(getKey(@"scale_gameplay"), @"Gameplay");
|
||||
|
||||
/// <summary>
|
||||
/// "Off"
|
||||
/// </summary>
|
||||
public static LocalisableString ScalingOff => new TranslatableString(getKey(@"scaling_off"), @"Off");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -74,6 +74,16 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString RestoreAllRecentlyDeletedBeatmaps => new TranslatableString(getKey(@"restore_all_recently_deleted_beatmaps"), @"Restore all recently deleted beatmaps");
|
||||
|
||||
/// <summary>
|
||||
/// "Delete ALL mod presets"
|
||||
/// </summary>
|
||||
public static LocalisableString DeleteAllModPresets => new TranslatableString(getKey(@"delete_all_mod_presets"), @"Delete ALL mod presets");
|
||||
|
||||
/// <summary>
|
||||
/// "Restore all recently deleted mod presets"
|
||||
/// </summary>
|
||||
public static LocalisableString RestoreAllRecentlyDeletedModPresets => new TranslatableString(getKey(@"restore_all_recently_deleted_mod_presets"), @"Restore all recently deleted mod presets");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString PersonalPresets => new TranslatableString(getKey(@"personal_presets"), @"Personal Presets");
|
||||
|
||||
/// <summary>
|
||||
/// "Add preset"
|
||||
/// </summary>
|
||||
public static LocalisableString AddPreset => new TranslatableString(getKey(@"add_preset"), @"Add preset");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,21 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString HighPrecisionPlatformWarning => new TranslatableString(getKey(@"high_precision_platform_warning"), @"This setting has known issues on your platform. If you encounter problems, it is recommended to adjust sensitivity externally and keep this disabled for now.");
|
||||
|
||||
/// <summary>
|
||||
/// "Always"
|
||||
/// </summary>
|
||||
public static LocalisableString AlwaysConfine => new TranslatableString(getKey(@"always_confine"), @"Always");
|
||||
|
||||
/// <summary>
|
||||
/// "During Gameplay"
|
||||
/// </summary>
|
||||
public static LocalisableString ConfineDuringGameplay => new TranslatableString(getKey(@"confine_during_gameplay"), @"During Gameplay");
|
||||
|
||||
/// <summary>
|
||||
/// "Never"
|
||||
/// </summary>
|
||||
public static LocalisableString NeverConfine => new TranslatableString(getKey(@"never_confine"), @"Never");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,21 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ShowExplicitContent => new TranslatableString(getKey(@"show_explicit_content"), @"Show explicit content in search results");
|
||||
|
||||
/// <summary>
|
||||
/// "Hide identifiable information"
|
||||
/// </summary>
|
||||
public static LocalisableString HideIdentifiableInformation => new TranslatableString(getKey(@"hide_identifiable_information"), @"Hide identifiable information");
|
||||
|
||||
/// <summary>
|
||||
/// "Full"
|
||||
/// </summary>
|
||||
public static LocalisableString DiscordPresenceFull => new TranslatableString(getKey(@"discord_presence_full"), @"Full");
|
||||
|
||||
/// <summary>
|
||||
/// "Off"
|
||||
/// </summary>
|
||||
public static LocalisableString DiscordPresenceOff => new TranslatableString(getKey(@"discord_presence_off"), @"Off");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,21 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString Rulesets => new TranslatableString(getKey(@"rulesets"), @"Rulesets");
|
||||
|
||||
/// <summary>
|
||||
/// "None"
|
||||
/// </summary>
|
||||
public static LocalisableString BorderNone => new TranslatableString(getKey(@"no_borders"), @"None");
|
||||
|
||||
/// <summary>
|
||||
/// "Corners"
|
||||
/// </summary>
|
||||
public static LocalisableString BorderCorners => new TranslatableString(getKey(@"corner_borders"), @"Corners");
|
||||
|
||||
/// <summary>
|
||||
/// "Full"
|
||||
/// </summary>
|
||||
public static LocalisableString BorderFull => new TranslatableString(getKey(@"full_borders"), @"Full");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
24
osu.Game/Localisation/SongSelectStrings.cs
Normal file
24
osu.Game/Localisation/SongSelectStrings.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class SongSelectStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.SongSelect";
|
||||
|
||||
/// <summary>
|
||||
/// "Local"
|
||||
/// </summary>
|
||||
public static LocalisableString LocallyModified => new TranslatableString(getKey(@"locally_modified"), @"Local");
|
||||
|
||||
/// <summary>
|
||||
/// "Has been locally modified"
|
||||
/// </summary>
|
||||
public static LocalisableString LocallyModifiedTooltip => new TranslatableString(getKey(@"locally_modified_tooltip"), @"Has been locally modified");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
24
osu.Game/Localisation/ToolbarStrings.cs
Normal file
24
osu.Game/Localisation/ToolbarStrings.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class ToolbarStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.Toolbar";
|
||||
|
||||
/// <summary>
|
||||
/// "Connection interrupted, will try to reconnect..."
|
||||
/// </summary>
|
||||
public static LocalisableString AttemptingToReconnect => new TranslatableString(getKey(@"attempting_to_reconnect"), @"Connection interrupted, will try to reconnect...");
|
||||
|
||||
/// <summary>
|
||||
/// "Connecting..."
|
||||
/// </summary>
|
||||
public static LocalisableString Connecting => new TranslatableString(getKey(@"connecting"), @"Connecting...");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
@ -114,6 +114,46 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString NoLimit => new TranslatableString(getKey(@"no_limit"), @"no limit");
|
||||
|
||||
/// <summary>
|
||||
/// "Beatmap (with storyboard / video)"
|
||||
/// </summary>
|
||||
public static LocalisableString BeatmapWithStoryboard => new TranslatableString(getKey(@"beatmap_with_storyboard"), @"Beatmap (with storyboard / video)");
|
||||
|
||||
/// <summary>
|
||||
/// "Always"
|
||||
/// </summary>
|
||||
public static LocalisableString AlwaysSeasonalBackground => new TranslatableString(getKey(@"always_seasonal_backgrounds"), @"Always");
|
||||
|
||||
/// <summary>
|
||||
/// "Never"
|
||||
/// </summary>
|
||||
public static LocalisableString NeverSeasonalBackground => new TranslatableString(getKey(@"never_seasonal_backgrounds"), @"Never");
|
||||
|
||||
/// <summary>
|
||||
/// "Sometimes"
|
||||
/// </summary>
|
||||
public static LocalisableString SometimesSeasonalBackground => new TranslatableString(getKey(@"sometimes_seasonal_backgrounds"), @"Sometimes");
|
||||
|
||||
/// <summary>
|
||||
/// "Sequential"
|
||||
/// </summary>
|
||||
public static LocalisableString SequentialHotkeyStyle => new TranslatableString(getKey(@"mods_sequential_hotkeys"), @"Sequential");
|
||||
|
||||
/// <summary>
|
||||
/// "Classic"
|
||||
/// </summary>
|
||||
public static LocalisableString ClassicHotkeyStyle => new TranslatableString(getKey(@"mods_classic_hotkeys"), @"Classic");
|
||||
|
||||
/// <summary>
|
||||
/// "Never repeat"
|
||||
/// </summary>
|
||||
public static LocalisableString NeverRepeat => new TranslatableString(getKey(@"never_repeat_random"), @"Never repeat");
|
||||
|
||||
/// <summary>
|
||||
/// "True Random"
|
||||
/// </summary>
|
||||
public static LocalisableString TrueRandom => new TranslatableString(getKey(@"true_random"), @"True Random");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -104,120 +104,39 @@ namespace osu.Game.Online.API
|
||||
/// </summary>
|
||||
private int failureCount;
|
||||
|
||||
/// <summary>
|
||||
/// The main API thread loop, which will continue to run until the game is shut down.
|
||||
/// </summary>
|
||||
private void run()
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
switch (State.Value)
|
||||
if (state.Value == APIState.Failing)
|
||||
{
|
||||
case APIState.Failing:
|
||||
//todo: replace this with a ping request.
|
||||
log.Add(@"In a failing state, waiting a bit before we try again...");
|
||||
Thread.Sleep(5000);
|
||||
// To recover from a failing state, falling through and running the full reconnection process seems safest for now.
|
||||
// This could probably be replaced with a ping-style request if we want to avoid the reconnection overheads.
|
||||
log.Add($@"{nameof(APIAccess)} is in a failing state, waiting a bit before we try again...");
|
||||
Thread.Sleep(5000);
|
||||
}
|
||||
|
||||
if (!IsLoggedIn) goto case APIState.Connecting;
|
||||
// Ensure that we have valid credentials.
|
||||
// If not, setting the offline state will allow the game to prompt the user to provide new credentials.
|
||||
if (!HasLogin)
|
||||
{
|
||||
state.Value = APIState.Offline;
|
||||
Thread.Sleep(50);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (queue.Count == 0)
|
||||
{
|
||||
log.Add(@"Queueing a ping request");
|
||||
Queue(new GetUserRequest());
|
||||
}
|
||||
Debug.Assert(HasLogin);
|
||||
|
||||
break;
|
||||
// Ensure that we are in an online state. If not, attempt a connect.
|
||||
if (state.Value != APIState.Online)
|
||||
{
|
||||
attemptConnect();
|
||||
|
||||
case APIState.Offline:
|
||||
case APIState.Connecting:
|
||||
// work to restore a connection...
|
||||
if (!HasLogin)
|
||||
{
|
||||
state.Value = APIState.Offline;
|
||||
Thread.Sleep(50);
|
||||
continue;
|
||||
}
|
||||
|
||||
state.Value = APIState.Connecting;
|
||||
|
||||
// save the username at this point, if the user requested for it to be.
|
||||
config.SetValue(OsuSetting.Username, config.Get<bool>(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty);
|
||||
|
||||
if (!authentication.HasValidAccessToken)
|
||||
{
|
||||
LastLoginError = null;
|
||||
|
||||
try
|
||||
{
|
||||
authentication.AuthenticateWithLogin(ProvidedUsername, password);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
//todo: this fails even on network-related issues. we should probably handle those differently.
|
||||
LastLoginError = e;
|
||||
log.Add(@"Login failed!");
|
||||
password = null;
|
||||
authentication.Clear();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var userReq = new GetUserRequest();
|
||||
|
||||
userReq.Failure += ex =>
|
||||
{
|
||||
if (ex is APIException)
|
||||
{
|
||||
LastLoginError = ex;
|
||||
log.Add("Login failed on local user retrieval!");
|
||||
Logout();
|
||||
}
|
||||
else if (ex is WebException webException && webException.Message == @"Unauthorized")
|
||||
{
|
||||
log.Add(@"Login no longer valid");
|
||||
Logout();
|
||||
}
|
||||
else
|
||||
failConnectionProcess();
|
||||
};
|
||||
userReq.Success += u =>
|
||||
{
|
||||
localUser.Value = u;
|
||||
|
||||
// todo: save/pull from settings
|
||||
localUser.Value.Status.Value = new UserStatusOnline();
|
||||
|
||||
failureCount = 0;
|
||||
};
|
||||
|
||||
if (!handleRequest(userReq))
|
||||
{
|
||||
failConnectionProcess();
|
||||
continue;
|
||||
}
|
||||
|
||||
// getting user's friends is considered part of the connection process.
|
||||
var friendsReq = new GetFriendsRequest();
|
||||
|
||||
friendsReq.Failure += _ => failConnectionProcess();
|
||||
friendsReq.Success += res =>
|
||||
{
|
||||
friends.AddRange(res);
|
||||
|
||||
//we're connected!
|
||||
state.Value = APIState.Online;
|
||||
};
|
||||
|
||||
if (!handleRequest(friendsReq))
|
||||
{
|
||||
failConnectionProcess();
|
||||
continue;
|
||||
}
|
||||
|
||||
// The Success callback event is fired on the main thread, so we should wait for that to run before proceeding.
|
||||
// Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests
|
||||
// before actually going online.
|
||||
while (State.Value > APIState.Offline && State.Value < APIState.Online)
|
||||
Thread.Sleep(500);
|
||||
|
||||
break;
|
||||
if (state.Value != APIState.Online)
|
||||
continue;
|
||||
}
|
||||
|
||||
// hard bail if we can't get a valid access token.
|
||||
@ -227,31 +146,132 @@ namespace osu.Game.Online.API
|
||||
continue;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
APIRequest req;
|
||||
|
||||
lock (queue)
|
||||
{
|
||||
if (queue.Count == 0) break;
|
||||
|
||||
req = queue.Dequeue();
|
||||
}
|
||||
|
||||
handleRequest(req);
|
||||
}
|
||||
|
||||
processQueuedRequests();
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
}
|
||||
|
||||
void failConnectionProcess()
|
||||
/// <summary>
|
||||
/// Dequeue from the queue and run each request synchronously until the queue is empty.
|
||||
/// </summary>
|
||||
private void processQueuedRequests()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// if something went wrong during the connection process, we want to reset the state (but only if still connecting).
|
||||
if (State.Value == APIState.Connecting)
|
||||
state.Value = APIState.Failing;
|
||||
APIRequest req;
|
||||
|
||||
lock (queue)
|
||||
{
|
||||
if (queue.Count == 0) return;
|
||||
|
||||
req = queue.Dequeue();
|
||||
}
|
||||
|
||||
handleRequest(req);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// From a non-connected state, perform a full connection flow, obtaining OAuth tokens and populating the local user and friends.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method takes control of <see cref="state"/> and transitions from <see cref="APIState.Connecting"/> to either
|
||||
/// - <see cref="APIState.Online"/> (successful connection)
|
||||
/// - <see cref="APIState.Failing"/> (failed connection but retrying)
|
||||
/// - <see cref="APIState.Offline"/> (failed and can't retry, clear credentials and require user interaction)
|
||||
/// </remarks>
|
||||
/// <returns>Whether the connection attempt was successful.</returns>
|
||||
private void attemptConnect()
|
||||
{
|
||||
state.Value = APIState.Connecting;
|
||||
|
||||
if (localUser.IsDefault)
|
||||
{
|
||||
// Show a placeholder user if saved credentials are available.
|
||||
// This is useful for storing local scores and showing a placeholder username after starting the game,
|
||||
// until a valid connection has been established.
|
||||
setLocalUser(new APIUser
|
||||
{
|
||||
Username = ProvidedUsername,
|
||||
});
|
||||
}
|
||||
|
||||
// save the username at this point, if the user requested for it to be.
|
||||
config.SetValue(OsuSetting.Username, config.Get<bool>(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty);
|
||||
|
||||
if (!authentication.HasValidAccessToken)
|
||||
{
|
||||
LastLoginError = null;
|
||||
|
||||
try
|
||||
{
|
||||
authentication.AuthenticateWithLogin(ProvidedUsername, password);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
//todo: this fails even on network-related issues. we should probably handle those differently.
|
||||
LastLoginError = e;
|
||||
log.Add($@"Login failed for username {ProvidedUsername} ({LastLoginError.Message})!");
|
||||
|
||||
Logout();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var userReq = new GetUserRequest();
|
||||
userReq.Failure += ex =>
|
||||
{
|
||||
if (ex is APIException)
|
||||
{
|
||||
LastLoginError = ex;
|
||||
log.Add($@"Login failed for username {ProvidedUsername} on user retrieval ({LastLoginError.Message})!");
|
||||
Logout();
|
||||
}
|
||||
else if (ex is WebException webException && webException.Message == @"Unauthorized")
|
||||
{
|
||||
log.Add(@"Login no longer valid");
|
||||
Logout();
|
||||
}
|
||||
else
|
||||
{
|
||||
state.Value = APIState.Failing;
|
||||
}
|
||||
};
|
||||
userReq.Success += user =>
|
||||
{
|
||||
// todo: save/pull from settings
|
||||
user.Status.Value = new UserStatusOnline();
|
||||
|
||||
setLocalUser(user);
|
||||
|
||||
// we're connected!
|
||||
state.Value = APIState.Online;
|
||||
failureCount = 0;
|
||||
};
|
||||
|
||||
if (!handleRequest(userReq))
|
||||
{
|
||||
state.Value = APIState.Failing;
|
||||
return;
|
||||
}
|
||||
|
||||
var friendsReq = new GetFriendsRequest();
|
||||
friendsReq.Failure += _ => state.Value = APIState.Failing;
|
||||
friendsReq.Success += res => friends.AddRange(res);
|
||||
|
||||
if (!handleRequest(friendsReq))
|
||||
{
|
||||
state.Value = APIState.Failing;
|
||||
return;
|
||||
}
|
||||
|
||||
// The Success callback event is fired on the main thread, so we should wait for that to run before proceeding.
|
||||
// Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests
|
||||
// before actually going online.
|
||||
while (State.Value == APIState.Connecting && !cancellationToken.IsCancellationRequested)
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
|
||||
public void Perform(APIRequest request)
|
||||
{
|
||||
try
|
||||
@ -327,8 +347,7 @@ namespace osu.Game.Online.API
|
||||
if (req.CompletionState != APIRequestCompletionState.Completed)
|
||||
return false;
|
||||
|
||||
// we could still be in initialisation, at which point we don't want to say we're Online yet.
|
||||
if (IsLoggedIn) state.Value = APIState.Online;
|
||||
// Reset failure count if this request succeeded.
|
||||
failureCount = 0;
|
||||
return true;
|
||||
}
|
||||
@ -402,7 +421,7 @@ namespace osu.Game.Online.API
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLoggedIn => localUser.Value.Id > 1; // TODO: should this also be true if attempting to connect?
|
||||
public bool IsLoggedIn => State.Value > APIState.Offline;
|
||||
|
||||
public void Queue(APIRequest request)
|
||||
{
|
||||
@ -442,7 +461,7 @@ namespace osu.Game.Online.API
|
||||
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
|
||||
Schedule(() =>
|
||||
{
|
||||
localUser.Value = createGuestUser();
|
||||
setLocalUser(createGuestUser());
|
||||
friends.Clear();
|
||||
});
|
||||
|
||||
@ -452,6 +471,8 @@ namespace osu.Game.Online.API
|
||||
|
||||
private static APIUser createGuestUser() => new GuestUser();
|
||||
|
||||
private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false);
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
@ -1,11 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
@ -23,7 +20,7 @@ namespace osu.Game.Online.API
|
||||
{
|
||||
[JsonProperty("acronym")]
|
||||
[Key(0)]
|
||||
public string Acronym { get; set; }
|
||||
public string Acronym { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("settings")]
|
||||
[Key(1)]
|
||||
@ -49,7 +46,7 @@ namespace osu.Game.Online.API
|
||||
}
|
||||
}
|
||||
|
||||
public Mod ToMod([NotNull] Ruleset ruleset)
|
||||
public Mod ToMod(Ruleset ruleset)
|
||||
{
|
||||
Mod resultMod = ruleset.CreateModFromAcronym(Acronym);
|
||||
|
||||
@ -80,6 +77,8 @@ namespace osu.Game.Online.API
|
||||
return resultMod;
|
||||
}
|
||||
|
||||
public bool ShouldSerializeSettings() => Settings.Count > 0;
|
||||
|
||||
public bool Equals(APIMod other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
|
@ -128,5 +128,13 @@ namespace osu.Game.Online.API
|
||||
IBindable<UserActivity> IAPIProvider.Activity => Activity;
|
||||
|
||||
public void FailNextLogin() => shouldFailNextLogin = true;
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
// Ensure (as much as we can) that any pending tasks are run.
|
||||
Scheduler.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,19 +13,16 @@ namespace osu.Game.Online.API
|
||||
{
|
||||
/// <summary>
|
||||
/// The local user.
|
||||
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
|
||||
/// </summary>
|
||||
IBindable<APIUser> LocalUser { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's friends.
|
||||
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
|
||||
/// </summary>
|
||||
IBindableList<APIUser> Friends { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current user's activity.
|
||||
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
|
||||
/// </summary>
|
||||
IBindable<UserActivity> Activity { get; }
|
||||
|
||||
|
@ -74,6 +74,9 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
[JsonProperty("statistics")]
|
||||
public Dictionary<HitResult, int> Statistics { get; set; } = new Dictionary<HitResult, int>();
|
||||
|
||||
[JsonProperty("maximum_statistics")]
|
||||
public Dictionary<HitResult, int> MaximumStatistics { get; set; } = new Dictionary<HitResult, int>();
|
||||
|
||||
#region osu-web API additions (not stored to database).
|
||||
|
||||
[JsonProperty("id")]
|
||||
@ -102,6 +105,14 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
[JsonProperty("pp")]
|
||||
public double? PP { get; set; }
|
||||
|
||||
public bool ShouldSerializeID() => false;
|
||||
public bool ShouldSerializeUser() => false;
|
||||
public bool ShouldSerializeBeatmap() => false;
|
||||
public bool ShouldSerializeBeatmapSet() => false;
|
||||
public bool ShouldSerializePP() => false;
|
||||
public bool ShouldSerializeOnlineID() => false;
|
||||
public bool ShouldSerializeHasReplay() => false;
|
||||
|
||||
#endregion
|
||||
|
||||
public override string ToString() => $"score_id: {ID} user_id: {UserID}";
|
||||
@ -145,6 +156,7 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
MaxCombo = MaxCombo,
|
||||
Rank = Rank,
|
||||
Statistics = Statistics,
|
||||
MaximumStatistics = MaximumStatistics,
|
||||
Date = EndedAt,
|
||||
Hash = HasReplay ? "online" : string.Empty, // TODO: temporary?
|
||||
Mods = mods,
|
||||
@ -165,7 +177,8 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
RulesetID = score.RulesetID,
|
||||
Passed = score.Passed,
|
||||
Mods = score.APIMods,
|
||||
Statistics = score.Statistics,
|
||||
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
||||
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
||||
};
|
||||
|
||||
public long OnlineID => ID ?? -1;
|
||||
|
@ -1,27 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Chat;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Online.Chat
|
||||
{
|
||||
public class ExternalLinkOpener : Component
|
||||
{
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
private GameHost host { get; set; } = null!;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDialogOverlay dialogOverlay { get; set; }
|
||||
private IDialogOverlay? dialogOverlay { get; set; }
|
||||
|
||||
private Bindable<bool> externalLinkWarning;
|
||||
private Bindable<bool> externalLinkWarning = null!;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuConfigManager config)
|
||||
@ -31,10 +31,39 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
public void OpenUrlExternally(string url, bool bypassWarning = false)
|
||||
{
|
||||
if (!bypassWarning && externalLinkWarning.Value)
|
||||
dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url)));
|
||||
if (!bypassWarning && externalLinkWarning.Value && dialogOverlay != null)
|
||||
dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => host.GetClipboard()?.SetText(url)));
|
||||
else
|
||||
host.OpenUrlExternally(url);
|
||||
}
|
||||
|
||||
public class ExternalLinkDialog : PopupDialog
|
||||
{
|
||||
public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction)
|
||||
{
|
||||
HeaderText = "Just checking...";
|
||||
BodyText = $"You are about to leave osu! and open the following link in a web browser:\n\n{url}";
|
||||
|
||||
Icon = FontAwesome.Solid.ExclamationTriangle;
|
||||
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogOkButton
|
||||
{
|
||||
Text = @"Yes. Go for it.",
|
||||
Action = openExternalLinkAction
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = @"Copy URL to the clipboard instead.",
|
||||
Action = copyExternalLinkAction
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = @"No! Abort mission!"
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ using osuTK;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -126,7 +128,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
private class HitResultCell : CompositeDrawable
|
||||
{
|
||||
private readonly string displayName;
|
||||
private readonly LocalisableString displayName;
|
||||
private readonly HitResult result;
|
||||
private readonly int count;
|
||||
|
||||
@ -134,7 +136,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
displayName = stat.DisplayName;
|
||||
displayName = stat.DisplayName.ToUpper();
|
||||
result = stat.Result;
|
||||
count = stat.Count;
|
||||
}
|
||||
@ -153,7 +155,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
|
||||
Text = displayName.ToUpperInvariant(),
|
||||
Text = displayName.ToUpper(),
|
||||
Colour = colours.ForHitResult(result),
|
||||
},
|
||||
new OsuSpriteText
|
||||
|
@ -196,6 +196,9 @@ namespace osu.Game.Online.Multiplayer
|
||||
APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem));
|
||||
APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId);
|
||||
|
||||
// The server will null out the end date upon the host joining the room, but the null value is never communicated to the client.
|
||||
APIRoom.EndDate.Value = null;
|
||||
|
||||
Debug.Assert(LocalUser != null);
|
||||
addUserToAPIRoom(LocalUser);
|
||||
|
||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Online
|
||||
/// <summary>
|
||||
/// A component which requires a constant polling process.
|
||||
/// </summary>
|
||||
public abstract class PollingComponent : CompositeDrawable // switch away from Component because InternalChildren are used in usages.
|
||||
public abstract class PollingComponent : CompositeComponent
|
||||
{
|
||||
private double? lastTimePolled;
|
||||
|
||||
|
@ -27,13 +27,10 @@ namespace osu.Game.Online.Rooms
|
||||
/// This differs from a regular download tracking composite as this accounts for the
|
||||
/// databased beatmap set's checksum, to disallow from playing with an altered version of the beatmap.
|
||||
/// </summary>
|
||||
public class OnlinePlayBeatmapAvailabilityTracker : CompositeDrawable
|
||||
public class OnlinePlayBeatmapAvailabilityTracker : CompositeComponent
|
||||
{
|
||||
public readonly IBindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
|
||||
|
||||
// Required to allow child components to update. Can potentially be replaced with a `CompositeComponent` class if or when we make one.
|
||||
protected override bool RequiresChildrenUpdate => true;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
|
@ -1138,6 +1138,13 @@ namespace osu.Game
|
||||
mouseDisableButtons.Value = !mouseDisableButtons.Value;
|
||||
return true;
|
||||
|
||||
case GlobalAction.ToggleProfile:
|
||||
if (userProfile.State.Value == Visibility.Visible)
|
||||
userProfile.Hide();
|
||||
else
|
||||
ShowUser(API.LocalUser.Value);
|
||||
return true;
|
||||
|
||||
case GlobalAction.RandomSkin:
|
||||
// Don't allow random skin selection while in the skin editor.
|
||||
// This is mainly to stop many "osu! default (modified)" skins being created via the SkinManager.EnsureMutableSkin() path.
|
||||
|
@ -248,7 +248,7 @@ namespace osu.Game
|
||||
|
||||
dependencies.CacheAs(Storage);
|
||||
|
||||
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")));
|
||||
var largeStore = new LargeTextureStore(Host.Renderer, Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")));
|
||||
largeStore.AddTextureSource(Host.CreateTextureLoaderStore(new OnlineStore()));
|
||||
dependencies.Cache(largeStore);
|
||||
|
||||
@ -588,6 +588,6 @@ namespace osu.Game
|
||||
|
||||
ControlPointInfo IBeatSyncProvider.ControlPoints => Beatmap.Value.BeatmapLoaded ? Beatmap.Value.Beatmap.ControlPointInfo : null;
|
||||
IClock IBeatSyncProvider.Clock => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track : (IClock)null;
|
||||
ChannelAmplitudes? IBeatSyncProvider.Amplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : null;
|
||||
ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : ChannelAmplitudes.Empty;
|
||||
}
|
||||
}
|
||||
|
@ -104,11 +104,11 @@ namespace osu.Game.Overlays
|
||||
filterControl.CardSize.BindValueChanged(_ => onCardSizeChanged());
|
||||
|
||||
apiUser = api.LocalUser.GetBoundCopy();
|
||||
apiUser.BindValueChanged(_ =>
|
||||
apiUser.BindValueChanged(_ => Schedule(() =>
|
||||
{
|
||||
if (api.IsLoggedIn)
|
||||
addContentToResultsArea(Drawable.Empty());
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
public void ShowWithSearch(string query)
|
||||
|
@ -147,7 +147,10 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
|
||||
{
|
||||
// beatmapset may have changed.
|
||||
if (Preview != preview)
|
||||
{
|
||||
preview?.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
AddInternal(preview);
|
||||
loading = false;
|
||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
/// <summary>
|
||||
/// The statistics that appear in the table, in order of appearance.
|
||||
/// </summary>
|
||||
private readonly List<(HitResult result, string displayName)> statisticResultTypes = new List<(HitResult, string)>();
|
||||
private readonly List<(HitResult result, LocalisableString displayName)> statisticResultTypes = new List<(HitResult, LocalisableString)>();
|
||||
|
||||
private bool showPerformancePoints;
|
||||
|
||||
@ -114,7 +114,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
if (result.IsBonus())
|
||||
continue;
|
||||
|
||||
string displayName = ruleset.GetDisplayNameForHitResult(result);
|
||||
var displayName = ruleset.GetDisplayNameForHitResult(result);
|
||||
|
||||
columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60)));
|
||||
statisticResultTypes.Add((result, displayName));
|
||||
|
@ -1,35 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Overlays.Chat
|
||||
{
|
||||
public class ExternalLinkDialog : PopupDialog
|
||||
{
|
||||
public ExternalLinkDialog(string url, Action openExternalLinkAction)
|
||||
{
|
||||
HeaderText = "Just checking...";
|
||||
BodyText = $"You are about to leave osu! and open the following link in a web browser:\n\n{url}";
|
||||
|
||||
Icon = FontAwesome.Solid.ExclamationTriangle;
|
||||
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogOkButton
|
||||
{
|
||||
Text = @"Yes. Go for it.",
|
||||
Action = openExternalLinkAction
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = @"No! Abort mission!"
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@
|
||||
#nullable disable
|
||||
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
using osu.Framework.Graphics.Containers.Markdown;
|
||||
using osu.Game.Graphics.Containers.Markdown;
|
||||
|
||||
@ -12,16 +11,8 @@ namespace osu.Game.Overlays.Comments
|
||||
{
|
||||
public class CommentMarkdownContainer : OsuMarkdownContainer
|
||||
{
|
||||
public override MarkdownTextFlowContainer CreateTextFlow() => new CommentMarkdownTextFlowContainer();
|
||||
|
||||
protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock);
|
||||
|
||||
private class CommentMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer
|
||||
{
|
||||
// Don't render image in comment for now
|
||||
protected override void AddImage(LinkInline linkInline) { }
|
||||
}
|
||||
|
||||
private class CommentMarkdownHeading : OsuMarkdownHeading
|
||||
{
|
||||
public CommentMarkdownHeading(HeadingBlock headingBlock)
|
||||
|
42
osu.Game/Overlays/Dialog/DeleteConfirmationDialog.cs
Normal file
42
osu.Game/Overlays/Dialog/DeleteConfirmationDialog.cs
Normal file
@ -0,0 +1,42 @@
|
||||
// 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.Localisation;
|
||||
|
||||
namespace osu.Game.Overlays.Dialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for various confirmation dialogs that concern deletion actions.
|
||||
/// Differs from <see cref="ConfirmDialog"/> in that the confirmation button is a "dangerous" one
|
||||
/// (requires the confirm button to be held).
|
||||
/// </summary>
|
||||
public abstract class DeleteConfirmationDialog : PopupDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// The action which performs the deletion.
|
||||
/// </summary>
|
||||
protected Action? DeleteAction { get; set; }
|
||||
|
||||
protected DeleteConfirmationDialog()
|
||||
{
|
||||
HeaderText = DeleteConfirmationDialogStrings.HeaderText;
|
||||
|
||||
Icon = FontAwesome.Regular.TrashAlt;
|
||||
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogDangerousButton
|
||||
{
|
||||
Text = DeleteConfirmationDialogStrings.Confirm,
|
||||
Action = () => DeleteAction?.Invoke()
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = DeleteConfirmationDialogStrings.Cancel
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
@ -109,7 +110,7 @@ namespace osu.Game.Overlays.Login
|
||||
Origin = Anchor.TopCentre,
|
||||
TextAnchor = Anchor.TopCentre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Text = state.NewValue == APIState.Failing ? "Connection is failing, will attempt to reconnect... " : "Attempting to connect... ",
|
||||
Text = state.NewValue == APIState.Failing ? ToolbarStrings.AttemptingToReconnect : ToolbarStrings.Connecting,
|
||||
Margin = new MarginPadding { Top = 10, Bottom = 10 },
|
||||
},
|
||||
};
|
||||
|
67
osu.Game/Overlays/Mods/AddPresetButton.cs
Normal file
67
osu.Game/Overlays/Mods/AddPresetButton.cs
Normal file
@ -0,0 +1,67 @@
|
||||
// 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 System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public class AddPresetButton : ShearedToggleButton, IHasPopover
|
||||
{
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; } = null!;
|
||||
|
||||
public AddPresetButton()
|
||||
: base(1)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = ModSelectPanel.HEIGHT;
|
||||
|
||||
// shear will be applied at a higher level in `ModPresetColumn`.
|
||||
Content.Shear = Vector2.Zero;
|
||||
Padding = new MarginPadding();
|
||||
|
||||
Text = "+";
|
||||
TextSize = 30;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
selectedMods.BindValueChanged(mods => Enabled.Value = mods.NewValue.Any(), true);
|
||||
Enabled.BindValueChanged(enabled =>
|
||||
{
|
||||
if (!enabled.NewValue)
|
||||
Active.Value = false;
|
||||
});
|
||||
}
|
||||
|
||||
protected override void UpdateActiveState()
|
||||
{
|
||||
DarkerColour = Active.Value ? colours.Orange1 : ColourProvider.Background3;
|
||||
LighterColour = Active.Value ? colours.Orange0 : ColourProvider.Background1;
|
||||
TextColour = Active.Value ? ColourProvider.Background6 : ColourProvider.Content1;
|
||||
|
||||
if (Active.Value)
|
||||
this.ShowPopover();
|
||||
else
|
||||
this.HidePopover();
|
||||
}
|
||||
|
||||
public Popover GetPopover() => new AddPresetPopover(this);
|
||||
}
|
||||
}
|
120
osu.Game/Overlays/Mods/AddPresetPopover.cs
Normal file
120
osu.Game/Overlays/Mods/AddPresetPopover.cs
Normal file
@ -0,0 +1,120 @@
|
||||
// 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 System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
internal class AddPresetPopover : OsuPopover
|
||||
{
|
||||
private readonly AddPresetButton button;
|
||||
|
||||
private readonly LabelledTextBox nameTextBox;
|
||||
private readonly LabelledTextBox descriptionTextBox;
|
||||
private readonly ShearedButton createButton;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
public AddPresetPopover(AddPresetButton addPresetButton)
|
||||
{
|
||||
button = addPresetButton;
|
||||
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Width = 300,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(7),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
nameTextBox = new LabelledTextBox
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Label = CommonStrings.Name,
|
||||
TabbableContentContainer = this
|
||||
},
|
||||
descriptionTextBox = new LabelledTextBox
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Label = CommonStrings.Description,
|
||||
TabbableContentContainer = this
|
||||
},
|
||||
createButton = new ShearedButton
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = ModSelectOverlayStrings.AddPreset,
|
||||
Action = tryCreatePreset
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider, OsuColour colours)
|
||||
{
|
||||
Body.BorderThickness = 3;
|
||||
Body.BorderColour = colours.Orange1;
|
||||
|
||||
createButton.DarkerColour = colours.Orange1;
|
||||
createButton.LighterColour = colours.Orange0;
|
||||
createButton.TextColour = colourProvider.Background6;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox));
|
||||
}
|
||||
|
||||
private void tryCreatePreset()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(nameTextBox.Current.Value))
|
||||
{
|
||||
Body.Shake();
|
||||
return;
|
||||
}
|
||||
|
||||
realm.Write(r => r.Add(new ModPreset
|
||||
{
|
||||
Name = nameTextBox.Current.Value,
|
||||
Description = descriptionTextBox.Current.Value,
|
||||
Mods = selectedMods.Value.ToArray(),
|
||||
Ruleset = r.Find<RulesetInfo>(ruleset.Value.ShortName)
|
||||
}));
|
||||
|
||||
this.HidePopover();
|
||||
}
|
||||
|
||||
protected override void UpdateState(ValueChangedEvent<Visibility> state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
if (state.NewValue == Visibility.Hidden)
|
||||
button.Active.Value = false;
|
||||
}
|
||||
}
|
||||
}
|
18
osu.Game/Overlays/Mods/DeleteModPresetDialog.cs
Normal file
18
osu.Game/Overlays/Mods/DeleteModPresetDialog.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.Game.Database;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public class DeleteModPresetDialog : DeleteConfirmationDialog
|
||||
{
|
||||
public DeleteModPresetDialog(Live<ModPreset> modPreset)
|
||||
{
|
||||
BodyText = modPreset.PerformRead(preset => preset.Name);
|
||||
DeleteAction = () => modPreset.PerformWrite(preset => preset.DeletePending = true);
|
||||
}
|
||||
}
|
||||
}
|
@ -31,10 +31,7 @@ namespace osu.Game.Overlays.Mods
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
private readonly BindableNumberWithCurrent<double> current = new BindableNumberWithCurrent<double>(1)
|
||||
{
|
||||
Precision = 0.01
|
||||
};
|
||||
private readonly BindableNumberWithCurrent<double> current = new BindableNumberWithCurrent<double>(1);
|
||||
|
||||
private readonly Box underlayBackground;
|
||||
private readonly Box contentBackground;
|
||||
|
@ -1,7 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Overlays.Mods.Input
|
||||
{
|
||||
@ -15,6 +17,7 @@ namespace osu.Game.Overlays.Mods.Input
|
||||
/// Individual letters in a row trigger the mods in a sequential fashion.
|
||||
/// Uses <see cref="SequentialModHotkeyHandler"/>.
|
||||
/// </summary>
|
||||
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.SequentialHotkeyStyle))]
|
||||
Sequential,
|
||||
|
||||
/// <summary>
|
||||
@ -22,6 +25,7 @@ namespace osu.Game.Overlays.Mods.Input
|
||||
/// One keybinding can toggle between what used to be <see cref="MultiMod"/>s on stable,
|
||||
/// and some mods in a column may not have any hotkeys at all.
|
||||
/// </summary>
|
||||
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.ClassicHotkeyStyle))]
|
||||
Classic
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,10 @@ namespace osu.Game.Overlays.Mods
|
||||
private IModHotkeyHandler hotkeyHandler = null!;
|
||||
|
||||
private Task? latestLoadTask;
|
||||
internal bool ItemsLoaded => latestLoadTask == null;
|
||||
private ICollection<ModPanel>? latestLoadedPanels;
|
||||
internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true && latestLoadedPanels?.All(panel => panel.Parent != null) == true;
|
||||
|
||||
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
|
||||
|
||||
public ModColumn(ModType modType, bool allowIncompatibleSelection)
|
||||
{
|
||||
@ -130,20 +133,14 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
cancellationTokenSource?.Cancel();
|
||||
|
||||
var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = Vector2.Zero));
|
||||
var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = Vector2.Zero)).ToArray();
|
||||
latestLoadedPanels = panels;
|
||||
|
||||
Task? loadTask;
|
||||
|
||||
latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded =>
|
||||
latestLoadTask = LoadComponentsAsync(panels, loaded =>
|
||||
{
|
||||
ItemsFlow.ChildrenEnumerable = loaded;
|
||||
updateState();
|
||||
}, (cancellationTokenSource = new CancellationTokenSource()).Token);
|
||||
loadTask.ContinueWith(_ =>
|
||||
{
|
||||
if (loadTask == latestLoadTask)
|
||||
latestLoadTask = null;
|
||||
});
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
|
@ -57,6 +57,18 @@ namespace osu.Game.Overlays.Mods
|
||||
Filtered.BindValueChanged(_ => updateFilterState(), true);
|
||||
}
|
||||
|
||||
protected override void Select()
|
||||
{
|
||||
modState.PendingConfiguration = Mod.RequiresConfiguration;
|
||||
Active.Value = true;
|
||||
}
|
||||
|
||||
protected override void Deselect()
|
||||
{
|
||||
modState.PendingConfiguration = false;
|
||||
Active.Value = false;
|
||||
}
|
||||
|
||||
#region Filtering support
|
||||
|
||||
private void updateFilterState()
|
||||
|
@ -2,12 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Localisation;
|
||||
@ -31,6 +31,10 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
AccentColour = colours.Orange1;
|
||||
HeaderText = ModSelectOverlayStrings.PersonalPresets;
|
||||
|
||||
AddPresetButton addPresetButton;
|
||||
ItemsFlow.Add(addPresetButton = new AddPresetButton());
|
||||
ItemsFlow.SetLayoutPosition(addPresetButton, float.PositiveInfinity);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -46,44 +50,41 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
presetSubscription?.Dispose();
|
||||
presetSubscription = realm.RegisterForNotifications(r =>
|
||||
r.All<ModPreset>()
|
||||
.Filter($"{nameof(ModPreset.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $0"
|
||||
+ $" && {nameof(ModPreset.DeletePending)} == false", ruleset.Value.ShortName)
|
||||
.OrderBy(preset => preset.Name),
|
||||
(presets, _, _) => asyncLoadPanels(presets));
|
||||
r.All<ModPreset>()
|
||||
.Filter($"{nameof(ModPreset.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $0"
|
||||
+ $" && {nameof(ModPreset.DeletePending)} == false", ruleset.Value.ShortName)
|
||||
.OrderBy(preset => preset.Name), asyncLoadPanels);
|
||||
}
|
||||
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
|
||||
private Task? latestLoadTask;
|
||||
internal bool ItemsLoaded => latestLoadTask == null;
|
||||
internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true;
|
||||
|
||||
private void asyncLoadPanels(IReadOnlyList<ModPreset> presets)
|
||||
private void asyncLoadPanels(IRealmCollection<ModPreset> presets, ChangeSet changes, Exception error)
|
||||
{
|
||||
cancellationTokenSource?.Cancel();
|
||||
|
||||
if (!presets.Any())
|
||||
{
|
||||
ItemsFlow.Clear();
|
||||
removeAndDisposePresetPanels();
|
||||
return;
|
||||
}
|
||||
|
||||
var panels = presets.Select(preset => new ModPresetPanel(preset.ToLive(realm))
|
||||
latestLoadTask = LoadComponentsAsync(presets.Select(p => new ModPresetPanel(p.ToLive(realm))
|
||||
{
|
||||
Shear = Vector2.Zero
|
||||
});
|
||||
|
||||
Task? loadTask;
|
||||
|
||||
latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded =>
|
||||
}), loaded =>
|
||||
{
|
||||
ItemsFlow.ChildrenEnumerable = loaded;
|
||||
removeAndDisposePresetPanels();
|
||||
ItemsFlow.AddRange(loaded);
|
||||
}, (cancellationTokenSource = new CancellationTokenSource()).Token);
|
||||
loadTask.ContinueWith(_ =>
|
||||
|
||||
void removeAndDisposePresetPanels()
|
||||
{
|
||||
if (loadTask == latestLoadTask)
|
||||
latestLoadTask = null;
|
||||
});
|
||||
foreach (var panel in ItemsFlow.OfType<ModPresetPanel>().ToArray())
|
||||
panel.RemoveAndDisposeImmediately();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -1,21 +1,36 @@
|
||||
// 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;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public class ModPresetPanel : ModSelectPanel, IHasCustomTooltip<ModPreset>
|
||||
public class ModPresetPanel : ModSelectPanel, IHasCustomTooltip<ModPreset>, IHasContextMenu
|
||||
{
|
||||
public readonly Live<ModPreset> Preset;
|
||||
|
||||
public override BindableBool Active { get; } = new BindableBool();
|
||||
|
||||
[Resolved]
|
||||
private IDialogOverlay? dialogOverlay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; } = null!;
|
||||
|
||||
private ModSettingChangeTracker? settingChangeTracker;
|
||||
|
||||
public ModPresetPanel(Live<ModPreset> preset)
|
||||
{
|
||||
Preset = preset;
|
||||
@ -30,7 +45,62 @@ namespace osu.Game.Overlays.Mods
|
||||
AccentColour = colours.Orange1;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
selectedMods.BindValueChanged(_ => selectedModsChanged(), true);
|
||||
}
|
||||
|
||||
protected override void Select()
|
||||
{
|
||||
// if the preset is not active at the point of the user click, then set the mods using the preset directly, discarding any previous selections,
|
||||
// which will also have the side effect of activating the preset (see `updateActiveState()`).
|
||||
selectedMods.Value = Preset.Value.Mods.ToArray();
|
||||
}
|
||||
|
||||
protected override void Deselect()
|
||||
{
|
||||
// if the preset is active when the user has clicked it, then it means that the set of active mods is exactly equal to the set of mods in the preset
|
||||
// (there are no other active mods than what the preset specifies, and the mod settings match exactly).
|
||||
// therefore it's safe to just clear selected mods, since it will have the effect of toggling the preset off.
|
||||
selectedMods.Value = Array.Empty<Mod>();
|
||||
}
|
||||
|
||||
private void selectedModsChanged()
|
||||
{
|
||||
settingChangeTracker?.Dispose();
|
||||
settingChangeTracker = new ModSettingChangeTracker(selectedMods.Value);
|
||||
settingChangeTracker.SettingChanged = _ => updateActiveState();
|
||||
updateActiveState();
|
||||
}
|
||||
|
||||
private void updateActiveState()
|
||||
{
|
||||
Active.Value = new HashSet<Mod>(Preset.Value.Mods).SetEquals(selectedMods.Value);
|
||||
}
|
||||
|
||||
#region IHasCustomTooltip
|
||||
|
||||
public ModPreset TooltipContent => Preset.Value;
|
||||
public ITooltip<ModPreset> GetCustomTooltip() => new ModPresetTooltip(ColourProvider);
|
||||
|
||||
#endregion
|
||||
|
||||
#region IHasContextMenu
|
||||
|
||||
public MenuItem[] ContextMenuItems => new MenuItem[]
|
||||
{
|
||||
new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new DeleteModPresetDialog(Preset)))
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
settingChangeTracker?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -159,12 +159,15 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
int wordIndex = 0;
|
||||
|
||||
headerText.AddText(text, t =>
|
||||
ITextPart part = headerText.AddText(text, t =>
|
||||
{
|
||||
if (wordIndex == 0)
|
||||
t.Font = t.Font.With(weight: FontWeight.SemiBold);
|
||||
wordIndex += 1;
|
||||
});
|
||||
|
||||
// Reset the index so that if the parts are refreshed (e.g. through changes in localisation) the correct word is re-emboldened.
|
||||
part.DrawablePartsRecreated += _ => wordIndex = 0;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -11,12 +11,14 @@ using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Localisation;
|
||||
@ -72,6 +74,11 @@ namespace osu.Game.Overlays.Mods
|
||||
/// </summary>
|
||||
protected virtual bool AllowCustomisation => true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the column with available mod presets should be shown.
|
||||
/// </summary>
|
||||
protected virtual bool ShowPresets => false;
|
||||
|
||||
protected virtual ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, false);
|
||||
|
||||
protected virtual IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection) => newSelection;
|
||||
@ -80,7 +87,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
if (AllowCustomisation)
|
||||
{
|
||||
yield return customisationButton = new ShearedToggleButton(BUTTON_WIDTH)
|
||||
yield return CustomisationButton = new ShearedToggleButton(BUTTON_WIDTH)
|
||||
{
|
||||
Text = ModSelectOverlayStrings.ModCustomisation,
|
||||
Active = { BindTarget = customisationVisible }
|
||||
@ -100,11 +107,11 @@ namespace osu.Game.Overlays.Mods
|
||||
private ColumnScrollContainer columnScroll = null!;
|
||||
private ColumnFlowContainer columnFlow = null!;
|
||||
private FillFlowContainer<ShearedButton> footerButtonFlow = null!;
|
||||
private ShearedButton backButton = null!;
|
||||
|
||||
private DifficultyMultiplierDisplay? multiplierDisplay;
|
||||
|
||||
private ShearedToggleButton? customisationButton;
|
||||
protected ShearedButton BackButton { get; private set; } = null!;
|
||||
protected ShearedToggleButton? CustomisationButton { get; private set; }
|
||||
|
||||
private Sample? columnAppearSample;
|
||||
|
||||
@ -139,40 +146,37 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
MainAreaContent.AddRange(new Drawable[]
|
||||
{
|
||||
new Container
|
||||
new OsuContextMenuContainer
|
||||
{
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Top = (ShowTotalMultiplier ? DifficultyMultiplierDisplay.HEIGHT : 0) + PADDING,
|
||||
Bottom = PADDING
|
||||
},
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
columnScroll = new ColumnScrollContainer
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = false,
|
||||
ClampExtension = 100,
|
||||
ScrollbarOverlapsContent = false,
|
||||
Child = columnFlow = new ColumnFlowContainer
|
||||
Top = (ShowTotalMultiplier ? DifficultyMultiplierDisplay.HEIGHT : 0) + PADDING,
|
||||
Bottom = PADDING
|
||||
},
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
columnScroll = new ColumnScrollContainer
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Shear = new Vector2(SHEAR, 0),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Margin = new MarginPadding { Horizontal = 70 },
|
||||
Padding = new MarginPadding { Bottom = 10 },
|
||||
Children = new[]
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = false,
|
||||
ClampExtension = 100,
|
||||
ScrollbarOverlapsContent = false,
|
||||
Child = columnFlow = new ColumnFlowContainer
|
||||
{
|
||||
createModColumnContent(ModType.DifficultyReduction),
|
||||
createModColumnContent(ModType.DifficultyIncrease),
|
||||
createModColumnContent(ModType.Automation),
|
||||
createModColumnContent(ModType.Conversion),
|
||||
createModColumnContent(ModType.Fun)
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Shear = new Vector2(SHEAR, 0),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Margin = new MarginPadding { Horizontal = 70 },
|
||||
Padding = new MarginPadding { Bottom = 10 },
|
||||
ChildrenEnumerable = createColumns()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -210,7 +214,7 @@ namespace osu.Game.Overlays.Mods
|
||||
Horizontal = 70
|
||||
},
|
||||
Spacing = new Vector2(10),
|
||||
ChildrenEnumerable = CreateFooterButtons().Prepend(backButton = new ShearedButton(BUTTON_WIDTH)
|
||||
ChildrenEnumerable = CreateFooterButtons().Prepend(BackButton = new ShearedButton(BUTTON_WIDTH)
|
||||
{
|
||||
Text = CommonStrings.Back,
|
||||
Action = Hide,
|
||||
@ -243,8 +247,8 @@ namespace osu.Game.Overlays.Mods
|
||||
modSettingChangeTracker?.Dispose();
|
||||
|
||||
updateMultiplier();
|
||||
updateCustomisation(val);
|
||||
updateFromExternalSelection();
|
||||
updateCustomisation();
|
||||
|
||||
if (AllowCustomisation)
|
||||
{
|
||||
@ -269,7 +273,7 @@ namespace osu.Game.Overlays.Mods
|
||||
/// </summary>
|
||||
public void SelectAll()
|
||||
{
|
||||
foreach (var column in columnFlow.Columns)
|
||||
foreach (var column in columnFlow.Columns.OfType<ModColumn>())
|
||||
column.SelectAll();
|
||||
}
|
||||
|
||||
@ -278,10 +282,27 @@ namespace osu.Game.Overlays.Mods
|
||||
/// </summary>
|
||||
public void DeselectAll()
|
||||
{
|
||||
foreach (var column in columnFlow.Columns)
|
||||
foreach (var column in columnFlow.Columns.OfType<ModColumn>())
|
||||
column.DeselectAll();
|
||||
}
|
||||
|
||||
private IEnumerable<ColumnDimContainer> createColumns()
|
||||
{
|
||||
if (ShowPresets)
|
||||
{
|
||||
yield return new ColumnDimContainer(new ModPresetColumn
|
||||
{
|
||||
Margin = new MarginPadding { Right = 10 }
|
||||
});
|
||||
}
|
||||
|
||||
yield return createModColumnContent(ModType.DifficultyReduction);
|
||||
yield return createModColumnContent(ModType.DifficultyIncrease);
|
||||
yield return createModColumnContent(ModType.Automation);
|
||||
yield return createModColumnContent(ModType.Conversion);
|
||||
yield return createModColumnContent(ModType.Fun);
|
||||
}
|
||||
|
||||
private ColumnDimContainer createModColumnContent(ModType modType)
|
||||
{
|
||||
var column = CreateModColumn(modType).With(column =>
|
||||
@ -290,12 +311,7 @@ namespace osu.Game.Overlays.Mods
|
||||
column.Margin = new MarginPadding { Right = 10 };
|
||||
});
|
||||
|
||||
return new ColumnDimContainer(column)
|
||||
{
|
||||
AutoSizeAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
RequestScroll = col => columnScroll.ScrollIntoView(col, extraScroll: 140),
|
||||
};
|
||||
return new ColumnDimContainer(column);
|
||||
}
|
||||
|
||||
private void createLocalMods()
|
||||
@ -317,7 +333,7 @@ namespace osu.Game.Overlays.Mods
|
||||
AvailableMods.Value = newLocalAvailableMods;
|
||||
filterMods();
|
||||
|
||||
foreach (var column in columnFlow.Columns)
|
||||
foreach (var column in columnFlow.Columns.OfType<ModColumn>())
|
||||
column.AvailableMods = AvailableMods.Value.GetValueOrDefault(column.ModType, Array.Empty<ModState>());
|
||||
}
|
||||
|
||||
@ -340,25 +356,26 @@ namespace osu.Game.Overlays.Mods
|
||||
multiplierDisplay.Current.Value = multiplier;
|
||||
}
|
||||
|
||||
private void updateCustomisation(ValueChangedEvent<IReadOnlyList<Mod>> valueChangedEvent)
|
||||
private void updateCustomisation()
|
||||
{
|
||||
if (customisationButton == null)
|
||||
if (CustomisationButton == null)
|
||||
return;
|
||||
|
||||
bool anyCustomisableMod = false;
|
||||
bool anyModWithRequiredCustomisationAdded = false;
|
||||
bool anyCustomisableModActive = false;
|
||||
bool anyModPendingConfiguration = false;
|
||||
|
||||
foreach (var mod in SelectedMods.Value)
|
||||
foreach (var modState in allAvailableMods)
|
||||
{
|
||||
anyCustomisableMod |= mod.GetSettingsSourceProperties().Any();
|
||||
anyModWithRequiredCustomisationAdded |= valueChangedEvent.OldValue.All(m => m.GetType() != mod.GetType()) && mod.RequiresConfiguration;
|
||||
anyCustomisableModActive |= modState.Active.Value && modState.Mod.GetSettingsSourceProperties().Any();
|
||||
anyModPendingConfiguration |= modState.PendingConfiguration;
|
||||
modState.PendingConfiguration = false;
|
||||
}
|
||||
|
||||
if (anyCustomisableMod)
|
||||
if (anyCustomisableModActive)
|
||||
{
|
||||
customisationVisible.Disabled = false;
|
||||
|
||||
if (anyModWithRequiredCustomisationAdded && !customisationVisible.Value)
|
||||
if (anyModPendingConfiguration && !customisationVisible.Value)
|
||||
customisationVisible.Value = true;
|
||||
}
|
||||
else
|
||||
@ -378,7 +395,7 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
foreach (var button in footerButtonFlow)
|
||||
{
|
||||
if (button != customisationButton)
|
||||
if (button != CustomisationButton)
|
||||
button.Enabled.Value = !customisationVisible.Value;
|
||||
}
|
||||
|
||||
@ -458,7 +475,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
var column = columnFlow[i].Column;
|
||||
|
||||
bool allFiltered = column.AvailableMods.All(modState => modState.Filtered.Value);
|
||||
bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => modState.Filtered.Value);
|
||||
|
||||
double delay = allFiltered ? 0 : nonFilteredColumnCount * 30;
|
||||
double duration = allFiltered ? 0 : fade_in_duration;
|
||||
@ -516,14 +533,19 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
var column = columnFlow[i].Column;
|
||||
|
||||
bool allFiltered = column.AvailableMods.All(modState => modState.Filtered.Value);
|
||||
bool allFiltered = false;
|
||||
|
||||
if (column is ModColumn modColumn)
|
||||
{
|
||||
allFiltered = modColumn.AvailableMods.All(modState => modState.Filtered.Value);
|
||||
modColumn.FlushPendingSelections();
|
||||
}
|
||||
|
||||
double duration = allFiltered ? 0 : fade_out_duration;
|
||||
float newYPosition = 0;
|
||||
if (!allFiltered)
|
||||
newYPosition = nonFilteredColumnCount % 2 == 0 ? -distance : distance;
|
||||
|
||||
column.FlushPendingSelections();
|
||||
column.TopLevelContent
|
||||
.MoveToY(newYPosition, duration, Easing.OutQuint)
|
||||
.FadeOut(duration, Easing.OutQuint);
|
||||
@ -566,14 +588,14 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
if (customisationVisible.Value)
|
||||
{
|
||||
Debug.Assert(customisationButton != null);
|
||||
customisationButton.TriggerClick();
|
||||
Debug.Assert(CustomisationButton != null);
|
||||
CustomisationButton.TriggerClick();
|
||||
|
||||
if (!immediate)
|
||||
return;
|
||||
}
|
||||
|
||||
backButton.TriggerClick();
|
||||
BackButton.TriggerClick();
|
||||
}
|
||||
}
|
||||
|
||||
@ -589,6 +611,7 @@ namespace osu.Game.Overlays.Mods
|
||||
/// <summary>
|
||||
/// Manages horizontal scrolling of mod columns, along with the "active" states of each column based on visibility.
|
||||
/// </summary>
|
||||
[Cached]
|
||||
internal class ColumnScrollContainer : OsuScrollContainer<ColumnFlowContainer>
|
||||
{
|
||||
public ColumnScrollContainer()
|
||||
@ -632,7 +655,7 @@ namespace osu.Game.Overlays.Mods
|
||||
/// </summary>
|
||||
internal class ColumnFlowContainer : FillFlowContainer<ColumnDimContainer>
|
||||
{
|
||||
public IEnumerable<ModColumn> Columns => Children.Select(dimWrapper => dimWrapper.Column);
|
||||
public IEnumerable<ModSelectColumn> Columns => Children.Select(dimWrapper => dimWrapper.Column);
|
||||
|
||||
public override void Add(ColumnDimContainer dimContainer)
|
||||
{
|
||||
@ -648,7 +671,7 @@ namespace osu.Game.Overlays.Mods
|
||||
/// </summary>
|
||||
internal class ColumnDimContainer : Container
|
||||
{
|
||||
public ModColumn Column { get; }
|
||||
public ModSelectColumn Column { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tracks whether this column is in an interactive state. Generally only the case when the column is on-screen.
|
||||
@ -663,12 +686,21 @@ namespace osu.Game.Overlays.Mods
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
public ColumnDimContainer(ModColumn column)
|
||||
public ColumnDimContainer(ModSelectColumn column)
|
||||
{
|
||||
AutoSizeAxes = Axes.X;
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
|
||||
Child = Column = column;
|
||||
column.Active.BindTo(Active);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ColumnScrollContainer columnScroll)
|
||||
{
|
||||
RequestScroll = col => columnScroll.ScrollIntoView(col, extraScroll: 140);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@ -677,7 +709,18 @@ namespace osu.Game.Overlays.Mods
|
||||
FinishTransforms();
|
||||
}
|
||||
|
||||
protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || Column.SelectionAnimationRunning;
|
||||
protected override bool RequiresChildrenUpdate
|
||||
{
|
||||
get
|
||||
{
|
||||
bool result = base.RequiresChildrenUpdate;
|
||||
|
||||
if (Column is ModColumn modColumn)
|
||||
result |= !modColumn.ItemsLoaded || modColumn.SelectionAnimationRunning;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
|
@ -43,8 +43,7 @@ namespace osu.Game.Overlays.Mods
|
||||
}
|
||||
|
||||
public const float CORNER_RADIUS = 7;
|
||||
|
||||
protected const float HEIGHT = 42;
|
||||
public const float HEIGHT = 42;
|
||||
|
||||
protected virtual float IdleSwitchWidth => 14;
|
||||
protected virtual float ExpandedSwitchWidth => 30;
|
||||
@ -144,9 +143,25 @@ namespace osu.Game.Overlays.Mods
|
||||
}
|
||||
};
|
||||
|
||||
Action = () => Active.Toggle();
|
||||
Action = () =>
|
||||
{
|
||||
if (!Active.Value)
|
||||
Select();
|
||||
else
|
||||
Deselect();
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs all actions necessary to select this <see cref="ModSelectPanel"/>.
|
||||
/// </summary>
|
||||
protected abstract void Select();
|
||||
|
||||
/// <summary>
|
||||
/// Performs all actions necessary to deselect this <see cref="ModSelectPanel"/>.
|
||||
/// </summary>
|
||||
protected abstract void Deselect();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio, ISamplePlaybackDisabler? samplePlaybackDisabler)
|
||||
{
|
||||
|
@ -24,6 +24,13 @@ namespace osu.Game.Overlays.Mods
|
||||
/// </summary>
|
||||
public BindableBool Active { get; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the mod requires further customisation.
|
||||
/// This flag is read by the <see cref="ModSelectOverlay"/> to determine if the customisation panel should be opened after a mod change
|
||||
/// and cleared after reading.
|
||||
/// </summary>
|
||||
public bool PendingConfiguration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the mod is currently filtered out due to not matching imposed criteria.
|
||||
/// </summary>
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
@ -17,10 +15,12 @@ namespace osu.Game.Overlays.Music
|
||||
{
|
||||
public class Playlist : OsuRearrangeableListContainer<Live<BeatmapSetInfo>>
|
||||
{
|
||||
public Action<Live<BeatmapSetInfo>> RequestSelection;
|
||||
public Action<Live<BeatmapSetInfo>>? RequestSelection;
|
||||
|
||||
public readonly Bindable<Live<BeatmapSetInfo>> SelectedSet = new Bindable<Live<BeatmapSetInfo>>();
|
||||
|
||||
private FilterCriteria currentCriteria = new FilterCriteria();
|
||||
|
||||
public new MarginPadding Padding
|
||||
{
|
||||
get => base.Padding;
|
||||
@ -31,26 +31,22 @@ namespace osu.Game.Overlays.Music
|
||||
{
|
||||
var items = (SearchContainer<RearrangeableListItem<Live<BeatmapSetInfo>>>)ListContainer;
|
||||
|
||||
string[] currentCollectionHashes = criteria.Collection?.PerformRead(c => c.BeatmapMD5Hashes.ToArray());
|
||||
string[]? currentCollectionHashes = criteria.Collection?.PerformRead(c => c.BeatmapMD5Hashes.ToArray());
|
||||
|
||||
foreach (var item in items.OfType<PlaylistItem>())
|
||||
{
|
||||
if (currentCollectionHashes == null)
|
||||
item.InSelectedCollection = true;
|
||||
else
|
||||
{
|
||||
item.InSelectedCollection = item.Model.Value.Beatmaps.Select(b => b.MD5Hash)
|
||||
.Any(currentCollectionHashes.Contains);
|
||||
}
|
||||
item.InSelectedCollection = currentCollectionHashes == null || item.Model.Value.Beatmaps.Select(b => b.MD5Hash).Any(currentCollectionHashes.Contains);
|
||||
}
|
||||
|
||||
items.SearchTerm = criteria.SearchText;
|
||||
currentCriteria = criteria;
|
||||
}
|
||||
|
||||
public Live<BeatmapSetInfo> FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter);
|
||||
public Live<BeatmapSetInfo>? FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter);
|
||||
|
||||
protected override OsuRearrangeableListItem<Live<BeatmapSetInfo>> CreateOsuDrawable(Live<BeatmapSetInfo> item) => new PlaylistItem(item)
|
||||
{
|
||||
InSelectedCollection = currentCriteria.Collection?.PerformRead(c => item.Value.Beatmaps.Select(b => b.MD5Hash).Any(c.BeatmapMD5Hashes.Contains)) != false,
|
||||
SelectedSet = { BindTarget = SelectedSet },
|
||||
RequestSelection = set => RequestSelection?.Invoke(set)
|
||||
};
|
||||
|
@ -26,8 +26,6 @@ namespace osu.Game.Overlays.Music
|
||||
private const float transition_duration = 600;
|
||||
private const float playlist_height = 510;
|
||||
|
||||
public IBindableList<Live<BeatmapSetInfo>> BeatmapSets => beatmapSets;
|
||||
|
||||
private readonly BindableList<Live<BeatmapSetInfo>> beatmapSets = new BindableList<Live<BeatmapSetInfo>>();
|
||||
|
||||
private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
|
||||
@ -104,9 +102,7 @@ namespace osu.Game.Overlays.Music
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
// tests might bind externally, in which case we don't want to involve realm.
|
||||
if (beatmapSets.Count == 0)
|
||||
beatmapSubscription = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>().Where(s => !s.DeletePending), beatmapsChanged);
|
||||
beatmapSubscription = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>().Where(s => !s.DeletePending), beatmapsChanged);
|
||||
|
||||
list.Items.BindTo(beatmapSets);
|
||||
beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true);
|
||||
|
@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
@ -70,85 +71,90 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
Masking = true,
|
||||
CornerRadius = avatar_size * 0.25f,
|
||||
},
|
||||
new Container
|
||||
new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Padding = new MarginPadding { Left = 10 },
|
||||
Children = new Drawable[]
|
||||
Child = new Container
|
||||
{
|
||||
new FillFlowContainer
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Padding = new MarginPadding { Left = 10 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
new FillFlowContainer
|
||||
{
|
||||
new FillFlowContainer
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
new FillFlowContainer
|
||||
{
|
||||
usernameText = new OsuSpriteText
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
|
||||
},
|
||||
openUserExternally = new ExternalLinkButton
|
||||
{
|
||||
Margin = new MarginPadding { Left = 5 },
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
}
|
||||
},
|
||||
titleText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Regular)
|
||||
},
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
supporterTag = new SupporterIcon
|
||||
{
|
||||
Height = 20,
|
||||
Margin = new MarginPadding { Top = 5 }
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 1.5f,
|
||||
Margin = new MarginPadding { Top = 10 },
|
||||
Colour = colourProvider.Light1,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Top = 5 },
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
userFlag = new UpdateableFlag
|
||||
{
|
||||
Size = new Vector2(28, 20),
|
||||
ShowPlaceholderOnUnknown = false,
|
||||
},
|
||||
userCountryText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 17.5f, weight: FontWeight.Regular),
|
||||
Margin = new MarginPadding { Left = 10 },
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Colour = colourProvider.Light1,
|
||||
usernameText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
|
||||
},
|
||||
openUserExternally = new ExternalLinkButton
|
||||
{
|
||||
Margin = new MarginPadding { Left = 5 },
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
titleText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Regular)
|
||||
},
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
supporterTag = new SupporterIcon
|
||||
{
|
||||
Height = 20,
|
||||
Margin = new MarginPadding { Top = 5 }
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 1.5f,
|
||||
Margin = new MarginPadding { Top = 10 },
|
||||
Colour = colourProvider.Light1,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Top = 5 },
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
userFlag = new UpdateableFlag
|
||||
{
|
||||
Size = new Vector2(28, 20),
|
||||
ShowPlaceholderOnUnknown = false,
|
||||
},
|
||||
userCountryText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 17.5f, weight: FontWeight.Regular),
|
||||
Margin = new MarginPadding { Left = 10 },
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Colour = colourProvider.Light1,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -120,7 +120,13 @@ namespace osu.Game.Overlays.Profile.Sections.Recent
|
||||
};
|
||||
|
||||
default:
|
||||
return Empty();
|
||||
return new RecentActivityIcon(activity)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 11,
|
||||
FillMode = FillMode.Fit,
|
||||
Margin = new MarginPadding { Top = 2, Vertical = 2 }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user