mirror of
https://github.com/osukey/osukey.git
synced 2025-08-04 23:24:04 +09:00
Merge branch 'master' into transformers-per-skin
This commit is contained in:
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Lists;
|
||||
@ -66,6 +67,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the difficulty control point at.</param>
|
||||
/// <returns>The difficulty control point.</returns>
|
||||
[NotNull]
|
||||
public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
|
||||
|
||||
/// <summary>
|
||||
@ -73,6 +75,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the effect control point at.</param>
|
||||
/// <returns>The effect control point.</returns>
|
||||
[NotNull]
|
||||
public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time, EffectControlPoint.DEFAULT);
|
||||
|
||||
/// <summary>
|
||||
@ -80,6 +83,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the sound control point at.</param>
|
||||
/// <returns>The sound control point.</returns>
|
||||
[NotNull]
|
||||
public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT);
|
||||
|
||||
/// <summary>
|
||||
@ -87,6 +91,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the timing control point at.</param>
|
||||
/// <returns>The timing control point.</returns>
|
||||
[NotNull]
|
||||
public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT);
|
||||
|
||||
/// <summary>
|
||||
|
16
osu.Game/Database/IHasGuidPrimaryKey.cs
Normal file
16
osu.Game/Database/IHasGuidPrimaryKey.cs
Normal file
@ -0,0 +1,16 @@
|
||||
// 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 Newtonsoft.Json;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public interface IHasGuidPrimaryKey
|
||||
{
|
||||
[JsonIgnore]
|
||||
[PrimaryKey]
|
||||
Guid ID { get; set; }
|
||||
}
|
||||
}
|
27
osu.Game/Database/IRealmFactory.cs
Normal file
27
osu.Game/Database/IRealmFactory.cs
Normal file
@ -0,0 +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.
|
||||
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public interface IRealmFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// The main realm context, bound to the update thread.
|
||||
/// </summary>
|
||||
Realm Context { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get a fresh context for read usage.
|
||||
/// </summary>
|
||||
RealmContextFactory.RealmUsage GetForRead();
|
||||
|
||||
/// <summary>
|
||||
/// Request a context for write usage.
|
||||
/// This method may block if a write is already active on a different thread.
|
||||
/// </summary>
|
||||
/// <returns>A usage containing a usable context.</returns>
|
||||
RealmContextFactory.RealmWriteUsage GetForWrite();
|
||||
}
|
||||
}
|
@ -24,13 +24,15 @@ namespace osu.Game.Database
|
||||
public DbSet<BeatmapDifficulty> BeatmapDifficulty { get; set; }
|
||||
public DbSet<BeatmapMetadata> BeatmapMetadata { get; set; }
|
||||
public DbSet<BeatmapSetInfo> BeatmapSetInfo { get; set; }
|
||||
public DbSet<DatabasedKeyBinding> DatabasedKeyBinding { get; set; }
|
||||
public DbSet<DatabasedSetting> DatabasedSetting { get; set; }
|
||||
public DbSet<FileInfo> FileInfo { get; set; }
|
||||
public DbSet<RulesetInfo> RulesetInfo { get; set; }
|
||||
public DbSet<SkinInfo> SkinInfo { get; set; }
|
||||
public DbSet<ScoreInfo> ScoreInfo { get; set; }
|
||||
|
||||
// migrated to realm
|
||||
public DbSet<DatabasedKeyBinding> DatabasedKeyBinding { get; set; }
|
||||
|
||||
private readonly string connectionString;
|
||||
|
||||
private static readonly Lazy<OsuDbLoggerFactory> logger = new Lazy<OsuDbLoggerFactory>(() => new OsuDbLoggerFactory());
|
||||
|
208
osu.Game/Database/RealmContextFactory.cs
Normal file
208
osu.Game/Database/RealmContextFactory.cs
Normal file
@ -0,0 +1,208 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Statistics;
|
||||
using osu.Game.Input.Bindings;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public class RealmContextFactory : Component, IRealmFactory
|
||||
{
|
||||
private readonly Storage storage;
|
||||
|
||||
private const string database_name = @"client";
|
||||
|
||||
private const int schema_version = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held for the duration of a write operation (via <see cref="GetForWrite"/>).
|
||||
/// </summary>
|
||||
private readonly object writeLock = new object();
|
||||
|
||||
private static readonly GlobalStatistic<int> reads = GlobalStatistics.Get<int>("Realm", "Get (Read)");
|
||||
private static readonly GlobalStatistic<int> writes = GlobalStatistics.Get<int>("Realm", "Get (Write)");
|
||||
private static readonly GlobalStatistic<int> refreshes = GlobalStatistics.Get<int>("Realm", "Dirty Refreshes");
|
||||
private static readonly GlobalStatistic<int> contexts_created = GlobalStatistics.Get<int>("Realm", "Contexts (Created)");
|
||||
private static readonly GlobalStatistic<int> pending_writes = GlobalStatistics.Get<int>("Realm", "Pending writes");
|
||||
private static readonly GlobalStatistic<int> active_usages = GlobalStatistics.Get<int>("Realm", "Active usages");
|
||||
|
||||
private readonly ManualResetEventSlim blockingResetEvent = new ManualResetEventSlim(true);
|
||||
|
||||
private Realm context;
|
||||
|
||||
public Realm Context
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsDisposed)
|
||||
throw new InvalidOperationException($"Attempted to access {nameof(Context)} on a disposed context factory");
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
context = createContext();
|
||||
Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}");
|
||||
}
|
||||
|
||||
// creating a context will ensure our schema is up-to-date and migrated.
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
public RealmContextFactory(Storage storage)
|
||||
{
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
public RealmUsage GetForRead()
|
||||
{
|
||||
reads.Value++;
|
||||
return new RealmUsage(this);
|
||||
}
|
||||
|
||||
public RealmWriteUsage GetForWrite()
|
||||
{
|
||||
writes.Value++;
|
||||
pending_writes.Value++;
|
||||
|
||||
Monitor.Enter(writeLock);
|
||||
|
||||
return new RealmWriteUsage(this);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (context?.Refresh() == true)
|
||||
refreshes.Value++;
|
||||
}
|
||||
|
||||
private Realm createContext()
|
||||
{
|
||||
blockingResetEvent.Wait();
|
||||
|
||||
contexts_created.Value++;
|
||||
|
||||
return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true))
|
||||
{
|
||||
SchemaVersion = schema_version,
|
||||
MigrationCallback = onMigration,
|
||||
});
|
||||
}
|
||||
|
||||
private void onMigration(Migration migration, ulong lastSchemaVersion)
|
||||
{
|
||||
switch (lastSchemaVersion)
|
||||
{
|
||||
case 5:
|
||||
// let's keep things simple. changing the type of the primary key is a bit involved.
|
||||
migration.NewRealm.RemoveAll<RealmKeyBinding>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
BlockAllOperations();
|
||||
}
|
||||
|
||||
public IDisposable BlockAllOperations()
|
||||
{
|
||||
blockingResetEvent.Reset();
|
||||
flushContexts();
|
||||
|
||||
return new InvokeOnDisposal<RealmContextFactory>(this, r => endBlockingSection());
|
||||
}
|
||||
|
||||
private void endBlockingSection()
|
||||
{
|
||||
blockingResetEvent.Set();
|
||||
}
|
||||
|
||||
private void flushContexts()
|
||||
{
|
||||
var previousContext = context;
|
||||
context = null;
|
||||
|
||||
// wait for all threaded usages to finish
|
||||
while (active_usages.Value > 0)
|
||||
Thread.Sleep(50);
|
||||
|
||||
previousContext?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A usage of realm from an arbitrary thread.
|
||||
/// </summary>
|
||||
public class RealmUsage : IDisposable
|
||||
{
|
||||
public readonly Realm Realm;
|
||||
|
||||
protected readonly RealmContextFactory Factory;
|
||||
|
||||
internal RealmUsage(RealmContextFactory factory)
|
||||
{
|
||||
active_usages.Value++;
|
||||
Factory = factory;
|
||||
Realm = factory.createContext();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes this instance, calling the initially captured action.
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
Realm?.Dispose();
|
||||
active_usages.Value--;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A transaction used for making changes to realm data.
|
||||
/// </summary>
|
||||
public class RealmWriteUsage : RealmUsage
|
||||
{
|
||||
private readonly Transaction transaction;
|
||||
|
||||
internal RealmWriteUsage(RealmContextFactory factory)
|
||||
: base(factory)
|
||||
{
|
||||
transaction = Realm.BeginWrite();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commit all changes made in this transaction.
|
||||
/// </summary>
|
||||
public void Commit() => transaction.Commit();
|
||||
|
||||
/// <summary>
|
||||
/// Revert all changes made in this transaction.
|
||||
/// </summary>
|
||||
public void Rollback() => transaction.Rollback();
|
||||
|
||||
/// <summary>
|
||||
/// Disposes this instance, calling the initially captured action.
|
||||
/// </summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
// rollback if not explicitly committed.
|
||||
transaction?.Dispose();
|
||||
|
||||
base.Dispose();
|
||||
|
||||
Monitor.Exit(Factory.writeLock);
|
||||
pending_writes.Value--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
51
osu.Game/Database/RealmExtensions.cs
Normal file
51
osu.Game/Database/RealmExtensions.cs
Normal file
@ -0,0 +1,51 @@
|
||||
// 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 AutoMapper;
|
||||
using osu.Game.Input.Bindings;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public static class RealmExtensions
|
||||
{
|
||||
private static readonly IMapper mapper = new MapperConfiguration(c =>
|
||||
{
|
||||
c.ShouldMapField = fi => false;
|
||||
c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic;
|
||||
|
||||
c.CreateMap<RealmKeyBinding, RealmKeyBinding>();
|
||||
}).CreateMapper();
|
||||
|
||||
/// <summary>
|
||||
/// Create a detached copy of the each item in the collection.
|
||||
/// </summary>
|
||||
/// <param name="items">A list of managed <see cref="RealmObject"/>s to detach.</param>
|
||||
/// <typeparam name="T">The type of object.</typeparam>
|
||||
/// <returns>A list containing non-managed copies of provided items.</returns>
|
||||
public static List<T> Detach<T>(this IEnumerable<T> items) where T : RealmObject
|
||||
{
|
||||
var list = new List<T>();
|
||||
|
||||
foreach (var obj in items)
|
||||
list.Add(obj.Detach());
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a detached copy of the item.
|
||||
/// </summary>
|
||||
/// <param name="item">The managed <see cref="RealmObject"/> to detach.</param>
|
||||
/// <typeparam name="T">The type of object.</typeparam>
|
||||
/// <returns>A non-managed copy of provided item. Will return the provided item if already detached.</returns>
|
||||
public static T Detach<T>(this T item) where T : RealmObject
|
||||
{
|
||||
if (!item.IsManaged)
|
||||
return item;
|
||||
|
||||
return mapper.Map<T>(item);
|
||||
}
|
||||
}
|
||||
}
|
33
osu.Game/Extensions/LanguageExtensions.cs
Normal file
33
osu.Game/Extensions/LanguageExtensions.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.Globalization;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Conversion utilities for the <see cref="Language"/> enum.
|
||||
/// </summary>
|
||||
public static class LanguageExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the culture code of the <see cref="CultureInfo"/> that corresponds to the supplied <paramref name="language"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is required as enum member names are not allowed to contain hyphens.
|
||||
/// </remarks>
|
||||
public static string ToCultureCode(this Language language)
|
||||
=> language.ToString().Replace("_", "-");
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse the supplied <paramref name="cultureCode"/> to a <see cref="Language"/> value.
|
||||
/// </summary>
|
||||
/// <param name="cultureCode">The code of the culture to parse.</param>
|
||||
/// <param name="language">The parsed <see cref="Language"/>. Valid only if the return value of the method is <see langword="true" />.</param>
|
||||
/// <returns>Whether the parsing succeeded.</returns>
|
||||
public static bool TryParseCultureCode(string cultureCode, out Language language)
|
||||
=> Enum.TryParse(cultureCode.Replace("-", "_"), out language);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osuTK.Graphics;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
@ -20,7 +21,8 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
protected virtual IEnumerable<Drawable> EffectTargets => new[] { Content };
|
||||
|
||||
public OsuHoverContainer()
|
||||
public OsuHoverContainer(HoverSampleSet sampleSet = HoverSampleSet.Default)
|
||||
: base(sampleSet)
|
||||
{
|
||||
Enabled.ValueChanged += e =>
|
||||
{
|
||||
|
@ -13,13 +13,13 @@ namespace osu.Game.Graphics.UserInterface
|
||||
[Description("button")]
|
||||
Button,
|
||||
|
||||
[Description("softer")]
|
||||
Soft,
|
||||
|
||||
[Description("toolbar")]
|
||||
Toolbar,
|
||||
|
||||
[Description("songselect")]
|
||||
SongSelect
|
||||
[Description("tabselect")]
|
||||
TabSelect,
|
||||
|
||||
[Description("scrolltotop")]
|
||||
ScrollToTop
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@
|
||||
using System.Linq;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -57,6 +59,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public override bool HandleNonPositionalInput => State == MenuState.Open;
|
||||
|
||||
private Sample sampleOpen;
|
||||
private Sample sampleClose;
|
||||
|
||||
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
|
||||
public OsuDropdownMenu()
|
||||
{
|
||||
@ -69,9 +74,30 @@ namespace osu.Game.Graphics.UserInterface
|
||||
ItemsContainer.Padding = new MarginPadding(5);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
sampleOpen = audio.Samples.Get(@"UI/dropdown-open");
|
||||
sampleClose = audio.Samples.Get(@"UI/dropdown-close");
|
||||
}
|
||||
|
||||
// todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed.
|
||||
private bool wasOpened;
|
||||
|
||||
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
|
||||
protected override void AnimateOpen() => this.FadeIn(300, Easing.OutQuint);
|
||||
protected override void AnimateClose() => this.FadeOut(300, Easing.OutQuint);
|
||||
protected override void AnimateOpen()
|
||||
{
|
||||
wasOpened = true;
|
||||
this.FadeIn(300, Easing.OutQuint);
|
||||
sampleOpen?.Play();
|
||||
}
|
||||
|
||||
protected override void AnimateClose()
|
||||
{
|
||||
this.FadeOut(300, Easing.OutQuint);
|
||||
if (wasOpened)
|
||||
sampleClose?.Play();
|
||||
}
|
||||
|
||||
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
|
||||
protected override void UpdateSize(Vector2 newSize)
|
||||
@ -155,7 +181,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
nonAccentSelectedColour = Color4.Black.Opacity(0.5f);
|
||||
updateColours();
|
||||
|
||||
AddInternal(new HoverClickSounds(HoverSampleSet.Soft));
|
||||
AddInternal(new HoverSounds());
|
||||
}
|
||||
|
||||
protected override void UpdateForegroundColour()
|
||||
@ -262,7 +288,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
},
|
||||
};
|
||||
|
||||
AddInternal(new HoverClickSounds());
|
||||
AddInternal(new HoverSounds());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -160,7 +160,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Margin = new MarginPadding { Top = 5, Bottom = 5 },
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Text = (value as IHasDescription)?.Description ?? (value as Enum)?.GetDescription() ?? value.ToString(),
|
||||
Text = (value as IHasDescription)?.Description ?? (value as Enum)?.GetLocalisableDescription() ?? value.ToString(),
|
||||
Font = OsuFont.GetFont(size: 14)
|
||||
},
|
||||
Bar = new Box
|
||||
@ -172,7 +172,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
},
|
||||
new HoverClickSounds()
|
||||
new HoverClickSounds(HoverSampleSet.TabSelect)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -43,6 +45,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
}
|
||||
|
||||
private const float transition_length = 500;
|
||||
private Sample sampleChecked;
|
||||
private Sample sampleUnchecked;
|
||||
|
||||
public OsuTabControlCheckbox()
|
||||
{
|
||||
@ -77,8 +81,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Colour = Color4.White,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
},
|
||||
new HoverClickSounds()
|
||||
}
|
||||
};
|
||||
|
||||
Current.ValueChanged += selected =>
|
||||
@ -91,10 +94,13 @@ namespace osu.Game.Graphics.UserInterface
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
private void load(OsuColour colours, AudioManager audio)
|
||||
{
|
||||
if (accentColour == null)
|
||||
AccentColour = colours.Blue;
|
||||
|
||||
sampleChecked = audio.Samples.Get(@"UI/check-on");
|
||||
sampleUnchecked = audio.Samples.Get(@"UI/check-off");
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
@ -111,6 +117,16 @@ namespace osu.Game.Graphics.UserInterface
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
protected override void OnUserChange(bool value)
|
||||
{
|
||||
base.OnUserChange(value);
|
||||
|
||||
if (value)
|
||||
sampleChecked?.Play();
|
||||
else
|
||||
sampleUnchecked?.Play();
|
||||
}
|
||||
|
||||
private void updateFade()
|
||||
{
|
||||
box.FadeTo(Current.Value || IsHovered ? 1 : 0, transition_length, Easing.OutQuint);
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
@ -75,13 +76,13 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
},
|
||||
new HoverClickSounds()
|
||||
new HoverClickSounds(HoverSampleSet.TabSelect)
|
||||
};
|
||||
|
||||
Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
|
||||
}
|
||||
|
||||
protected virtual string CreateText() => (Value as Enum)?.GetDescription() ?? Value.ToString();
|
||||
protected virtual LocalisableString CreateText() => (Value as Enum)?.GetLocalisableDescription() ?? Value.ToString();
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
|
@ -33,12 +33,18 @@ namespace osu.Game.IO
|
||||
private readonly StorageConfigManager storageConfig;
|
||||
private readonly Storage defaultStorage;
|
||||
|
||||
public override string[] IgnoreDirectories => new[] { "cache" };
|
||||
public override string[] IgnoreDirectories => new[]
|
||||
{
|
||||
"cache",
|
||||
"client.realm.management"
|
||||
};
|
||||
|
||||
public override string[] IgnoreFiles => new[]
|
||||
{
|
||||
"framework.ini",
|
||||
"storage.ini"
|
||||
"storage.ini",
|
||||
"client.realm.note",
|
||||
"client.realm.lock",
|
||||
};
|
||||
|
||||
public OsuStorage(GameHost host, Storage defaultStorage)
|
||||
|
@ -8,7 +8,7 @@ using osu.Game.Database;
|
||||
namespace osu.Game.Input.Bindings
|
||||
{
|
||||
[Table("KeyBinding")]
|
||||
public class DatabasedKeyBinding : KeyBinding, IHasPrimaryKey
|
||||
public class DatabasedKeyBinding : IKeyBinding, IHasPrimaryKey
|
||||
{
|
||||
public int ID { get; set; }
|
||||
|
||||
@ -17,17 +17,23 @@ namespace osu.Game.Input.Bindings
|
||||
public int? Variant { get; set; }
|
||||
|
||||
[Column("Keys")]
|
||||
public string KeysString
|
||||
{
|
||||
get => KeyCombination.ToString();
|
||||
private set => KeyCombination = value;
|
||||
}
|
||||
public string KeysString { get; set; }
|
||||
|
||||
[Column("Action")]
|
||||
public int IntAction
|
||||
public int IntAction { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public KeyCombination KeyCombination
|
||||
{
|
||||
get => (int)Action;
|
||||
set => Action = value;
|
||||
get => KeysString;
|
||||
set => KeysString = value.ToString();
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
public object Action
|
||||
{
|
||||
get => IntAction;
|
||||
set => IntAction = (int)value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,12 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets;
|
||||
using System.Linq;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Input.Bindings
|
||||
{
|
||||
@ -21,7 +23,11 @@ namespace osu.Game.Input.Bindings
|
||||
|
||||
private readonly int? variant;
|
||||
|
||||
private KeyBindingStore store;
|
||||
private IDisposable realmSubscription;
|
||||
private IQueryable<RealmKeyBinding> realmKeyBindings;
|
||||
|
||||
[Resolved]
|
||||
private RealmContextFactory realmFactory { get; set; }
|
||||
|
||||
public override IEnumerable<IKeyBinding> DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0);
|
||||
|
||||
@ -42,24 +48,34 @@ namespace osu.Game.Input.Bindings
|
||||
throw new InvalidOperationException($"{nameof(variant)} can not be null when a non-null {nameof(ruleset)} is provided.");
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(KeyBindingStore keyBindings)
|
||||
{
|
||||
store = keyBindings;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
if (ruleset == null || ruleset.ID.HasValue)
|
||||
{
|
||||
var rulesetId = ruleset?.ID;
|
||||
|
||||
realmKeyBindings = realmFactory.Context.All<RealmKeyBinding>()
|
||||
.Where(b => b.RulesetID == rulesetId && b.Variant == variant);
|
||||
|
||||
realmSubscription = realmKeyBindings
|
||||
.SubscribeForNotifications((sender, changes, error) =>
|
||||
{
|
||||
// first subscription ignored as we are handling this in LoadComplete.
|
||||
if (changes == null)
|
||||
return;
|
||||
|
||||
ReloadMappings();
|
||||
});
|
||||
}
|
||||
|
||||
base.LoadComplete();
|
||||
store.KeyBindingChanged += ReloadMappings;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (store != null)
|
||||
store.KeyBindingChanged -= ReloadMappings;
|
||||
realmSubscription?.Dispose();
|
||||
}
|
||||
|
||||
protected override void ReloadMappings()
|
||||
@ -67,17 +83,17 @@ namespace osu.Game.Input.Bindings
|
||||
var defaults = DefaultKeyBindings.ToList();
|
||||
|
||||
if (ruleset != null && !ruleset.ID.HasValue)
|
||||
// if the provided ruleset is not stored to the database, we have no way to retrieve custom bindings.
|
||||
// fallback to defaults instead.
|
||||
// some tests instantiate a ruleset which is not present in the database.
|
||||
// in these cases we still want key bindings to work, but matching to database instances would result in none being present,
|
||||
// so let's populate the defaults directly.
|
||||
KeyBindings = defaults;
|
||||
else
|
||||
{
|
||||
KeyBindings = store.Query(ruleset?.ID, variant)
|
||||
.OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.IntAction))
|
||||
// this ordering is important to ensure that we read entries from the database in the order
|
||||
// enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise
|
||||
// have been eaten by the music controller due to query order.
|
||||
.ToList();
|
||||
KeyBindings = realmKeyBindings.Detach()
|
||||
// this ordering is important to ensure that we read entries from the database in the order
|
||||
// enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise
|
||||
// have been eaten by the music controller due to query order.
|
||||
.OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.ActionInt)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
39
osu.Game/Input/Bindings/RealmKeyBinding.cs
Normal file
39
osu.Game/Input/Bindings/RealmKeyBinding.cs
Normal file
@ -0,0 +1,39 @@
|
||||
// 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.Input.Bindings;
|
||||
using osu.Game.Database;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Input.Bindings
|
||||
{
|
||||
[MapTo(nameof(KeyBinding))]
|
||||
public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey, IKeyBinding
|
||||
{
|
||||
[PrimaryKey]
|
||||
public Guid ID { get; set; } = Guid.NewGuid();
|
||||
|
||||
public int? RulesetID { get; set; }
|
||||
|
||||
public int? Variant { get; set; }
|
||||
|
||||
public KeyCombination KeyCombination
|
||||
{
|
||||
get => KeyCombinationString;
|
||||
set => KeyCombinationString = value.ToString();
|
||||
}
|
||||
|
||||
public object Action
|
||||
{
|
||||
get => ActionInt;
|
||||
set => ActionInt = (int)value;
|
||||
}
|
||||
|
||||
[MapTo(nameof(Action))]
|
||||
public int ActionInt { get; set; }
|
||||
|
||||
[MapTo(nameof(KeyCombination))]
|
||||
public string KeyCombinationString { get; set; }
|
||||
}
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Input
|
||||
{
|
||||
public class KeyBindingStore : DatabaseBackedStore
|
||||
{
|
||||
public event Action KeyBindingChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Keys which should not be allowed for gameplay input purposes.
|
||||
/// </summary>
|
||||
private static readonly IEnumerable<InputKey> banned_keys = new[]
|
||||
{
|
||||
InputKey.MouseWheelDown,
|
||||
InputKey.MouseWheelLeft,
|
||||
InputKey.MouseWheelUp,
|
||||
InputKey.MouseWheelRight
|
||||
};
|
||||
|
||||
public KeyBindingStore(DatabaseContextFactory contextFactory, RulesetStore rulesets, Storage storage = null)
|
||||
: base(contextFactory, storage)
|
||||
{
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
foreach (var info in rulesets.AvailableRulesets)
|
||||
{
|
||||
var ruleset = info.CreateInstance();
|
||||
foreach (var variant in ruleset.AvailableVariants)
|
||||
insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action.
|
||||
/// </summary>
|
||||
/// <param name="globalAction">The action to lookup.</param>
|
||||
/// <returns>A set of display strings for all the user's key configuration for the action.</returns>
|
||||
public IEnumerable<string> GetReadableKeyCombinationsFor(GlobalAction globalAction)
|
||||
{
|
||||
foreach (var action in Query().Where(b => (GlobalAction)b.Action == globalAction))
|
||||
{
|
||||
string str = action.KeyCombination.ReadableString();
|
||||
|
||||
// even if found, the readable string may be empty for an unbound action.
|
||||
if (str.Length > 0)
|
||||
yield return str;
|
||||
}
|
||||
}
|
||||
|
||||
private void insertDefaults(IEnumerable<IKeyBinding> defaults, int? rulesetId = null, int? variant = null)
|
||||
{
|
||||
using (var usage = ContextFactory.GetForWrite())
|
||||
{
|
||||
// compare counts in database vs defaults
|
||||
foreach (var group in defaults.GroupBy(k => k.Action))
|
||||
{
|
||||
int count = Query(rulesetId, variant).Count(k => (int)k.Action == (int)group.Key);
|
||||
int aimCount = group.Count();
|
||||
|
||||
if (aimCount <= count)
|
||||
continue;
|
||||
|
||||
foreach (var insertable in group.Skip(count).Take(aimCount - count))
|
||||
{
|
||||
// insert any defaults which are missing.
|
||||
usage.Context.DatabasedKeyBinding.Add(new DatabasedKeyBinding
|
||||
{
|
||||
KeyCombination = insertable.KeyCombination,
|
||||
Action = insertable.Action,
|
||||
RulesetID = rulesetId,
|
||||
Variant = variant
|
||||
});
|
||||
|
||||
// required to ensure stable insert order (https://github.com/dotnet/efcore/issues/11686)
|
||||
usage.Context.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve <see cref="DatabasedKeyBinding"/>s for a specified ruleset/variant content.
|
||||
/// </summary>
|
||||
/// <param name="rulesetId">The ruleset's internal ID.</param>
|
||||
/// <param name="variant">An optional variant.</param>
|
||||
public List<DatabasedKeyBinding> Query(int? rulesetId = null, int? variant = null) =>
|
||||
ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
|
||||
|
||||
public void Update(KeyBinding keyBinding)
|
||||
{
|
||||
using (ContextFactory.GetForWrite())
|
||||
{
|
||||
var dbKeyBinding = (DatabasedKeyBinding)keyBinding;
|
||||
|
||||
Debug.Assert(dbKeyBinding.RulesetID == null || CheckValidForGameplay(keyBinding.KeyCombination));
|
||||
|
||||
Refresh(ref dbKeyBinding);
|
||||
|
||||
if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination))
|
||||
return;
|
||||
|
||||
dbKeyBinding.KeyCombination = keyBinding.KeyCombination;
|
||||
}
|
||||
|
||||
KeyBindingChanged?.Invoke();
|
||||
}
|
||||
|
||||
public static bool CheckValidForGameplay(KeyCombination combination)
|
||||
{
|
||||
foreach (var key in banned_keys)
|
||||
{
|
||||
if (combination.Keys.Contains(key))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
117
osu.Game/Input/RealmKeyBindingStore.cs
Normal file
117
osu.Game/Input/RealmKeyBindingStore.cs
Normal file
@ -0,0 +1,117 @@
|
||||
// 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.Input.Bindings;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Input
|
||||
{
|
||||
public class RealmKeyBindingStore
|
||||
{
|
||||
private readonly RealmContextFactory realmFactory;
|
||||
|
||||
public RealmKeyBindingStore(RealmContextFactory realmFactory)
|
||||
{
|
||||
this.realmFactory = realmFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action.
|
||||
/// </summary>
|
||||
/// <param name="globalAction">The action to lookup.</param>
|
||||
/// <returns>A set of display strings for all the user's key configuration for the action.</returns>
|
||||
public IReadOnlyList<string> GetReadableKeyCombinationsFor(GlobalAction globalAction)
|
||||
{
|
||||
List<string> combinations = new List<string>();
|
||||
|
||||
using (var context = realmFactory.GetForRead())
|
||||
{
|
||||
foreach (var action in context.Realm.All<RealmKeyBinding>().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction))
|
||||
{
|
||||
string str = action.KeyCombination.ReadableString();
|
||||
|
||||
// even if found, the readable string may be empty for an unbound action.
|
||||
if (str.Length > 0)
|
||||
combinations.Add(str);
|
||||
}
|
||||
}
|
||||
|
||||
return combinations;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a new type of <see cref="KeyBindingContainer{T}"/>, adding default bindings from <see cref="KeyBindingContainer.DefaultKeyBindings"/>.
|
||||
/// </summary>
|
||||
/// <param name="container">The container to populate defaults from.</param>
|
||||
public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings);
|
||||
|
||||
/// <summary>
|
||||
/// Register a ruleset, adding default bindings for each of its variants.
|
||||
/// </summary>
|
||||
/// <param name="ruleset">The ruleset to populate defaults from.</param>
|
||||
public void Register(RulesetInfo ruleset)
|
||||
{
|
||||
var instance = ruleset.CreateInstance();
|
||||
|
||||
foreach (var variant in instance.AvailableVariants)
|
||||
insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant);
|
||||
}
|
||||
|
||||
private void insertDefaults(IEnumerable<IKeyBinding> defaults, int? rulesetId = null, int? variant = null)
|
||||
{
|
||||
using (var usage = realmFactory.GetForWrite())
|
||||
{
|
||||
// compare counts in database vs defaults
|
||||
foreach (var defaultsForAction in defaults.GroupBy(k => k.Action))
|
||||
{
|
||||
int existingCount = usage.Realm.All<RealmKeyBinding>().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key);
|
||||
|
||||
if (defaultsForAction.Count() <= existingCount)
|
||||
continue;
|
||||
|
||||
foreach (var k in defaultsForAction.Skip(existingCount))
|
||||
{
|
||||
// insert any defaults which are missing.
|
||||
usage.Realm.Add(new RealmKeyBinding
|
||||
{
|
||||
KeyCombinationString = k.KeyCombination.ToString(),
|
||||
ActionInt = (int)k.Action,
|
||||
RulesetID = rulesetId,
|
||||
Variant = variant
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
usage.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keys which should not be allowed for gameplay input purposes.
|
||||
/// </summary>
|
||||
private static readonly IEnumerable<InputKey> banned_keys = new[]
|
||||
{
|
||||
InputKey.MouseWheelDown,
|
||||
InputKey.MouseWheelLeft,
|
||||
InputKey.MouseWheelUp,
|
||||
InputKey.MouseWheelRight
|
||||
};
|
||||
|
||||
public static bool CheckValidForGameplay(KeyCombination combination)
|
||||
{
|
||||
foreach (var key in banned_keys)
|
||||
{
|
||||
if (combination.Keys.Contains(key))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -10,7 +10,103 @@ namespace osu.Game.Localisation
|
||||
[Description(@"English")]
|
||||
en,
|
||||
|
||||
// TODO: Requires Arabic glyphs to be added to resources (and possibly also RTL support).
|
||||
// [Description(@"اَلْعَرَبِيَّةُ")]
|
||||
// ar,
|
||||
|
||||
[Description(@"Беларуская мова")]
|
||||
be,
|
||||
|
||||
[Description(@"Български")]
|
||||
bg,
|
||||
|
||||
[Description(@"Česky")]
|
||||
cs,
|
||||
|
||||
[Description(@"Dansk")]
|
||||
da,
|
||||
|
||||
[Description(@"Deutsch")]
|
||||
de,
|
||||
|
||||
[Description(@"Ελληνικά")]
|
||||
el,
|
||||
|
||||
[Description(@"español")]
|
||||
es,
|
||||
|
||||
[Description(@"Suomi")]
|
||||
fi,
|
||||
|
||||
[Description(@"français")]
|
||||
fr,
|
||||
|
||||
[Description(@"Magyar")]
|
||||
hu,
|
||||
|
||||
[Description(@"Bahasa Indonesia")]
|
||||
id,
|
||||
|
||||
[Description(@"Italiano")]
|
||||
it,
|
||||
|
||||
[Description(@"日本語")]
|
||||
ja
|
||||
ja,
|
||||
|
||||
[Description(@"한국어")]
|
||||
ko,
|
||||
|
||||
[Description(@"Nederlands")]
|
||||
nl,
|
||||
|
||||
[Description(@"Norsk")]
|
||||
no,
|
||||
|
||||
[Description(@"polski")]
|
||||
pl,
|
||||
|
||||
[Description(@"Português")]
|
||||
pt,
|
||||
|
||||
[Description(@"Português (Brasil)")]
|
||||
pt_br,
|
||||
|
||||
[Description(@"Română")]
|
||||
ro,
|
||||
|
||||
[Description(@"Русский")]
|
||||
ru,
|
||||
|
||||
[Description(@"Slovenčina")]
|
||||
sk,
|
||||
|
||||
[Description(@"Svenska")]
|
||||
sv,
|
||||
|
||||
[Description(@"ไทย")]
|
||||
th,
|
||||
|
||||
// Tagalog has no associated localisations yet, and is not supported on Xamarin platforms or Windows versions <10.
|
||||
// Can be revisited if localisations ever arrive.
|
||||
//[Description(@"Tagalog")]
|
||||
//tl,
|
||||
|
||||
[Description(@"Türkçe")]
|
||||
tr,
|
||||
|
||||
[Description(@"Українська мова")]
|
||||
uk,
|
||||
|
||||
[Description(@"Tiếng Việt")]
|
||||
vi,
|
||||
|
||||
[Description(@"简体中文")]
|
||||
zh,
|
||||
|
||||
[Description(@"繁體中文(香港)")]
|
||||
zh_hk,
|
||||
|
||||
[Description(@"繁體中文(台灣)")]
|
||||
zh_tw
|
||||
}
|
||||
}
|
||||
|
@ -154,6 +154,10 @@ namespace osu.Game.Online.Chat
|
||||
case "beatmapsets":
|
||||
case "d":
|
||||
{
|
||||
if (mainArg == "discussions")
|
||||
// handle discussion links externally for now
|
||||
return new LinkDetails(LinkAction.External, url);
|
||||
|
||||
if (args.Length > 4 && int.TryParse(args[4], out var id))
|
||||
// https://osu.ppy.sh/beatmapsets/1154158#osu/2768184
|
||||
return new LinkDetails(LinkAction.OpenBeatmap, id.ToString());
|
||||
|
@ -6,7 +6,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using MessagePack;
|
||||
using osu.Game.Online.API;
|
||||
|
||||
@ -28,11 +27,9 @@ namespace osu.Game.Online.Multiplayer
|
||||
[Key(3)]
|
||||
public string Name { get; set; } = "Unnamed room";
|
||||
|
||||
[NotNull]
|
||||
[Key(4)]
|
||||
public IEnumerable<APIMod> RequiredMods { get; set; } = Enumerable.Empty<APIMod>();
|
||||
|
||||
[NotNull]
|
||||
[Key(5)]
|
||||
public IEnumerable<APIMod> AllowedMods { get; set; } = Enumerable.Empty<APIMod>();
|
||||
|
||||
|
@ -6,7 +6,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Online.API;
|
||||
@ -35,7 +34,6 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// Any mods applicable only to the local user.
|
||||
/// </summary>
|
||||
[Key(3)]
|
||||
[NotNull]
|
||||
public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>();
|
||||
|
||||
[IgnoreMember]
|
||||
|
@ -50,8 +50,10 @@ using osu.Game.Updater;
|
||||
using osu.Game.Utils;
|
||||
using LogLevel = osu.Framework.Logging.LogLevel;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Performance;
|
||||
using osu.Game.Skinning.Editor;
|
||||
|
||||
namespace osu.Game
|
||||
@ -487,6 +489,8 @@ namespace osu.Game
|
||||
|
||||
protected virtual UpdateManager CreateUpdateManager() => new UpdateManager();
|
||||
|
||||
protected virtual HighPerformanceSession CreateHighPerformanceSession() => new HighPerformanceSession();
|
||||
|
||||
protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything);
|
||||
|
||||
#region Beatmap progression
|
||||
@ -580,8 +584,16 @@ namespace osu.Game
|
||||
|
||||
foreach (var language in Enum.GetValues(typeof(Language)).OfType<Language>())
|
||||
{
|
||||
var cultureCode = language.ToString();
|
||||
Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode));
|
||||
var cultureCode = language.ToCultureCode();
|
||||
|
||||
try
|
||||
{
|
||||
Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, $"Could not load localisations for language \"{cultureCode}\"");
|
||||
}
|
||||
}
|
||||
|
||||
// The next time this is updated is in UpdateAfterChildren, which occurs too late and results
|
||||
@ -604,9 +616,9 @@ namespace osu.Game
|
||||
|
||||
LocalConfig.LookupKeyBindings = l =>
|
||||
{
|
||||
var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l).ToArray();
|
||||
var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l);
|
||||
|
||||
if (combinations.Length == 0)
|
||||
if (combinations.Count == 0)
|
||||
return "none";
|
||||
|
||||
return string.Join(" or ", combinations);
|
||||
@ -755,6 +767,8 @@ namespace osu.Game
|
||||
loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true);
|
||||
loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true);
|
||||
|
||||
loadComponentSingleFile(CreateHighPerformanceSession(), Add);
|
||||
|
||||
chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible;
|
||||
|
||||
Add(difficultyRecommender);
|
||||
|
@ -95,7 +95,7 @@ namespace osu.Game
|
||||
|
||||
protected RulesetStore RulesetStore { get; private set; }
|
||||
|
||||
protected KeyBindingStore KeyBindingStore { get; private set; }
|
||||
protected RealmKeyBindingStore KeyBindingStore { get; private set; }
|
||||
|
||||
protected MenuCursorContainer MenuCursorContainer { get; private set; }
|
||||
|
||||
@ -144,6 +144,8 @@ namespace osu.Game
|
||||
|
||||
private DatabaseContextFactory contextFactory;
|
||||
|
||||
private RealmContextFactory realmFactory;
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
private Container content;
|
||||
@ -179,6 +181,9 @@ namespace osu.Game
|
||||
|
||||
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
|
||||
|
||||
dependencies.Cache(realmFactory = new RealmContextFactory(Storage));
|
||||
AddInternal(realmFactory);
|
||||
|
||||
dependencies.CacheAs(Storage);
|
||||
|
||||
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")));
|
||||
@ -190,20 +195,29 @@ namespace osu.Game
|
||||
|
||||
AddFont(Resources, @"Fonts/osuFont");
|
||||
|
||||
AddFont(Resources, @"Fonts/Torus-Regular");
|
||||
AddFont(Resources, @"Fonts/Torus-Light");
|
||||
AddFont(Resources, @"Fonts/Torus-SemiBold");
|
||||
AddFont(Resources, @"Fonts/Torus-Bold");
|
||||
AddFont(Resources, @"Fonts/Torus/Torus-Regular");
|
||||
AddFont(Resources, @"Fonts/Torus/Torus-Light");
|
||||
AddFont(Resources, @"Fonts/Torus/Torus-SemiBold");
|
||||
AddFont(Resources, @"Fonts/Torus/Torus-Bold");
|
||||
|
||||
AddFont(Resources, @"Fonts/Noto-Basic");
|
||||
AddFont(Resources, @"Fonts/Noto-Hangul");
|
||||
AddFont(Resources, @"Fonts/Noto-CJK-Basic");
|
||||
AddFont(Resources, @"Fonts/Noto-CJK-Compatibility");
|
||||
AddFont(Resources, @"Fonts/Noto-Thai");
|
||||
AddFont(Resources, @"Fonts/Inter/Inter-Regular");
|
||||
AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic");
|
||||
AddFont(Resources, @"Fonts/Inter/Inter-Light");
|
||||
AddFont(Resources, @"Fonts/Inter/Inter-LightItalic");
|
||||
AddFont(Resources, @"Fonts/Inter/Inter-SemiBold");
|
||||
AddFont(Resources, @"Fonts/Inter/Inter-SemiBoldItalic");
|
||||
AddFont(Resources, @"Fonts/Inter/Inter-Bold");
|
||||
AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic");
|
||||
|
||||
AddFont(Resources, @"Fonts/Venera-Light");
|
||||
AddFont(Resources, @"Fonts/Venera-Bold");
|
||||
AddFont(Resources, @"Fonts/Venera-Black");
|
||||
AddFont(Resources, @"Fonts/Noto/Noto-Basic");
|
||||
AddFont(Resources, @"Fonts/Noto/Noto-Hangul");
|
||||
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic");
|
||||
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility");
|
||||
AddFont(Resources, @"Fonts/Noto/Noto-Thai");
|
||||
|
||||
AddFont(Resources, @"Fonts/Venera/Venera-Light");
|
||||
AddFont(Resources, @"Fonts/Venera/Venera-Bold");
|
||||
AddFont(Resources, @"Fonts/Venera/Venera-Black");
|
||||
|
||||
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
|
||||
|
||||
@ -275,7 +289,8 @@ namespace osu.Game
|
||||
dependencies.Cache(scorePerformanceManager);
|
||||
AddInternal(scorePerformanceManager);
|
||||
|
||||
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
|
||||
migrateDataToRealm();
|
||||
|
||||
dependencies.Cache(settingsStore = new SettingsStore(contextFactory));
|
||||
dependencies.Cache(rulesetConfigCache = new RulesetConfigCache(settingsStore));
|
||||
|
||||
@ -323,7 +338,12 @@ namespace osu.Game
|
||||
|
||||
base.Content.Add(CreateScalingContainer().WithChildren(mainContent));
|
||||
|
||||
KeyBindingStore = new RealmKeyBindingStore(realmFactory);
|
||||
KeyBindingStore.Register(globalBindings);
|
||||
|
||||
foreach (var r in RulesetStore.AvailableRulesets)
|
||||
KeyBindingStore.Register(r);
|
||||
|
||||
dependencies.Cache(globalBindings);
|
||||
|
||||
PreviewTrackManager previewTrackManager;
|
||||
@ -378,8 +398,11 @@ namespace osu.Game
|
||||
|
||||
public void Migrate(string path)
|
||||
{
|
||||
contextFactory.FlushConnections();
|
||||
(Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
|
||||
using (realmFactory.BlockAllOperations())
|
||||
{
|
||||
contextFactory.FlushConnections();
|
||||
(Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
|
||||
}
|
||||
}
|
||||
|
||||
protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager();
|
||||
@ -390,6 +413,34 @@ namespace osu.Game
|
||||
|
||||
protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage);
|
||||
|
||||
private void migrateDataToRealm()
|
||||
{
|
||||
using (var db = contextFactory.GetForWrite())
|
||||
using (var usage = realmFactory.GetForWrite())
|
||||
{
|
||||
var existingBindings = db.Context.DatabasedKeyBinding;
|
||||
|
||||
// only migrate data if the realm database is empty.
|
||||
if (!usage.Realm.All<RealmKeyBinding>().Any())
|
||||
{
|
||||
foreach (var dkb in existingBindings)
|
||||
{
|
||||
usage.Realm.Add(new RealmKeyBinding
|
||||
{
|
||||
KeyCombinationString = dkb.KeyCombination.ToString(),
|
||||
ActionInt = (int)dkb.Action,
|
||||
RulesetID = dkb.RulesetID,
|
||||
Variant = dkb.Variant
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
db.Context.RemoveRange(existingBindings);
|
||||
|
||||
usage.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> r)
|
||||
{
|
||||
var dict = new Dictionary<ModType, IReadOnlyList<Mod>>();
|
||||
|
@ -1,9 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Scoring;
|
||||
@ -33,38 +33,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
}
|
||||
|
||||
protected override LocalisableString LabelFor(ScoreRank value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case ScoreRank.XH:
|
||||
return BeatmapsStrings.RankXH;
|
||||
|
||||
case ScoreRank.X:
|
||||
return BeatmapsStrings.RankX;
|
||||
|
||||
case ScoreRank.SH:
|
||||
return BeatmapsStrings.RankSH;
|
||||
|
||||
case ScoreRank.S:
|
||||
return BeatmapsStrings.RankS;
|
||||
|
||||
case ScoreRank.A:
|
||||
return BeatmapsStrings.RankA;
|
||||
|
||||
case ScoreRank.B:
|
||||
return BeatmapsStrings.RankB;
|
||||
|
||||
case ScoreRank.C:
|
||||
return BeatmapsStrings.RankC;
|
||||
|
||||
case ScoreRank.D:
|
||||
return BeatmapsStrings.RankD;
|
||||
|
||||
default:
|
||||
throw new ArgumentException("Unsupported value.", nameof(value));
|
||||
}
|
||||
}
|
||||
protected override LocalisableString LabelFor(ScoreRank value) => value.GetLocalisableDescription();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
/// <summary>
|
||||
/// Returns the label text to be used for the supplied <paramref name="value"/>.
|
||||
/// </summary>
|
||||
protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetDescription() ?? value.ToString();
|
||||
protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetLocalisableDescription() ?? value.ToString();
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
|
@ -1,10 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
[LocalisableEnum(typeof(SearchCategoryEnumLocalisationMapper))]
|
||||
public enum SearchCategory
|
||||
{
|
||||
Any,
|
||||
@ -23,4 +27,43 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
[Description("My Maps")]
|
||||
Mine,
|
||||
}
|
||||
|
||||
public class SearchCategoryEnumLocalisationMapper : EnumLocalisationMapper<SearchCategory>
|
||||
{
|
||||
public override LocalisableString Map(SearchCategory value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SearchCategory.Any:
|
||||
return BeatmapsStrings.StatusAny;
|
||||
|
||||
case SearchCategory.Leaderboard:
|
||||
return BeatmapsStrings.StatusLeaderboard;
|
||||
|
||||
case SearchCategory.Ranked:
|
||||
return BeatmapsStrings.StatusRanked;
|
||||
|
||||
case SearchCategory.Qualified:
|
||||
return BeatmapsStrings.StatusQualified;
|
||||
|
||||
case SearchCategory.Loved:
|
||||
return BeatmapsStrings.StatusLoved;
|
||||
|
||||
case SearchCategory.Favourites:
|
||||
return BeatmapsStrings.StatusFavourites;
|
||||
|
||||
case SearchCategory.Pending:
|
||||
return BeatmapsStrings.StatusPending;
|
||||
|
||||
case SearchCategory.Graveyard:
|
||||
return BeatmapsStrings.StatusGraveyard;
|
||||
|
||||
case SearchCategory.Mine:
|
||||
return BeatmapsStrings.StatusMine;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,34 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
[LocalisableEnum(typeof(SearchExplicitEnumLocalisationMapper))]
|
||||
public enum SearchExplicit
|
||||
{
|
||||
Hide,
|
||||
Show
|
||||
}
|
||||
|
||||
public class SearchExplicitEnumLocalisationMapper : EnumLocalisationMapper<SearchExplicit>
|
||||
{
|
||||
public override LocalisableString Map(SearchExplicit value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SearchExplicit.Hide:
|
||||
return BeatmapsStrings.NsfwExclude;
|
||||
|
||||
case SearchExplicit.Show:
|
||||
return BeatmapsStrings.NsfwInclude;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
[LocalisableEnum(typeof(SearchExtraEnumLocalisationMapper))]
|
||||
public enum SearchExtra
|
||||
{
|
||||
[Description("Has Video")]
|
||||
@ -13,4 +17,22 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
[Description("Has Storyboard")]
|
||||
Storyboard
|
||||
}
|
||||
|
||||
public class SearchExtraEnumLocalisationMapper : EnumLocalisationMapper<SearchExtra>
|
||||
{
|
||||
public override LocalisableString Map(SearchExtra value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SearchExtra.Video:
|
||||
return BeatmapsStrings.ExtraVideo;
|
||||
|
||||
case SearchExtra.Storyboard:
|
||||
return BeatmapsStrings.ExtraStoryboard;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
[LocalisableEnum(typeof(SearchGeneralEnumLocalisationMapper))]
|
||||
public enum SearchGeneral
|
||||
{
|
||||
[Description("Recommended difficulty")]
|
||||
@ -16,4 +20,25 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
[Description("Subscribed mappers")]
|
||||
Follows
|
||||
}
|
||||
|
||||
public class SearchGeneralEnumLocalisationMapper : EnumLocalisationMapper<SearchGeneral>
|
||||
{
|
||||
public override LocalisableString Map(SearchGeneral value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SearchGeneral.Recommended:
|
||||
return BeatmapsStrings.GeneralRecommended;
|
||||
|
||||
case SearchGeneral.Converts:
|
||||
return BeatmapsStrings.GeneralConverts;
|
||||
|
||||
case SearchGeneral.Follows:
|
||||
return BeatmapsStrings.GeneralFollows;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
[LocalisableEnum(typeof(SearchGenreEnumLocalisationMapper))]
|
||||
public enum SearchGenre
|
||||
{
|
||||
Any = 0,
|
||||
@ -26,4 +30,58 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
Folk = 13,
|
||||
Jazz = 14
|
||||
}
|
||||
|
||||
public class SearchGenreEnumLocalisationMapper : EnumLocalisationMapper<SearchGenre>
|
||||
{
|
||||
public override LocalisableString Map(SearchGenre value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SearchGenre.Any:
|
||||
return BeatmapsStrings.GenreAny;
|
||||
|
||||
case SearchGenre.Unspecified:
|
||||
return BeatmapsStrings.GenreUnspecified;
|
||||
|
||||
case SearchGenre.VideoGame:
|
||||
return BeatmapsStrings.GenreVideoGame;
|
||||
|
||||
case SearchGenre.Anime:
|
||||
return BeatmapsStrings.GenreAnime;
|
||||
|
||||
case SearchGenre.Rock:
|
||||
return BeatmapsStrings.GenreRock;
|
||||
|
||||
case SearchGenre.Pop:
|
||||
return BeatmapsStrings.GenrePop;
|
||||
|
||||
case SearchGenre.Other:
|
||||
return BeatmapsStrings.GenreOther;
|
||||
|
||||
case SearchGenre.Novelty:
|
||||
return BeatmapsStrings.GenreNovelty;
|
||||
|
||||
case SearchGenre.HipHop:
|
||||
return BeatmapsStrings.GenreHipHop;
|
||||
|
||||
case SearchGenre.Electronic:
|
||||
return BeatmapsStrings.GenreElectronic;
|
||||
|
||||
case SearchGenre.Metal:
|
||||
return BeatmapsStrings.GenreMetal;
|
||||
|
||||
case SearchGenre.Classical:
|
||||
return BeatmapsStrings.GenreClassical;
|
||||
|
||||
case SearchGenre.Folk:
|
||||
return BeatmapsStrings.GenreFolk;
|
||||
|
||||
case SearchGenre.Jazz:
|
||||
return BeatmapsStrings.GenreJazz;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
[LocalisableEnum(typeof(SearchLanguageEnumLocalisationMapper))]
|
||||
[HasOrderedElements]
|
||||
public enum SearchLanguage
|
||||
{
|
||||
@ -53,4 +57,61 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
[Order(13)]
|
||||
Other
|
||||
}
|
||||
|
||||
public class SearchLanguageEnumLocalisationMapper : EnumLocalisationMapper<SearchLanguage>
|
||||
{
|
||||
public override LocalisableString Map(SearchLanguage value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SearchLanguage.Any:
|
||||
return BeatmapsStrings.LanguageAny;
|
||||
|
||||
case SearchLanguage.Unspecified:
|
||||
return BeatmapsStrings.LanguageUnspecified;
|
||||
|
||||
case SearchLanguage.English:
|
||||
return BeatmapsStrings.LanguageEnglish;
|
||||
|
||||
case SearchLanguage.Japanese:
|
||||
return BeatmapsStrings.LanguageJapanese;
|
||||
|
||||
case SearchLanguage.Chinese:
|
||||
return BeatmapsStrings.LanguageChinese;
|
||||
|
||||
case SearchLanguage.Instrumental:
|
||||
return BeatmapsStrings.LanguageInstrumental;
|
||||
|
||||
case SearchLanguage.Korean:
|
||||
return BeatmapsStrings.LanguageKorean;
|
||||
|
||||
case SearchLanguage.French:
|
||||
return BeatmapsStrings.LanguageFrench;
|
||||
|
||||
case SearchLanguage.German:
|
||||
return BeatmapsStrings.LanguageGerman;
|
||||
|
||||
case SearchLanguage.Swedish:
|
||||
return BeatmapsStrings.LanguageSwedish;
|
||||
|
||||
case SearchLanguage.Spanish:
|
||||
return BeatmapsStrings.LanguageSpanish;
|
||||
|
||||
case SearchLanguage.Italian:
|
||||
return BeatmapsStrings.LanguageItalian;
|
||||
|
||||
case SearchLanguage.Russian:
|
||||
return BeatmapsStrings.LanguageRussian;
|
||||
|
||||
case SearchLanguage.Polish:
|
||||
return BeatmapsStrings.LanguagePolish;
|
||||
|
||||
case SearchLanguage.Other:
|
||||
return BeatmapsStrings.LanguageOther;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,38 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
[LocalisableEnum(typeof(SearchPlayedEnumLocalisationMapper))]
|
||||
public enum SearchPlayed
|
||||
{
|
||||
Any,
|
||||
Played,
|
||||
Unplayed
|
||||
}
|
||||
|
||||
public class SearchPlayedEnumLocalisationMapper : EnumLocalisationMapper<SearchPlayed>
|
||||
{
|
||||
public override LocalisableString Map(SearchPlayed value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SearchPlayed.Any:
|
||||
return BeatmapsStrings.PlayedAny;
|
||||
|
||||
case SearchPlayed.Played:
|
||||
return BeatmapsStrings.PlayedPlayed;
|
||||
|
||||
case SearchPlayed.Unplayed:
|
||||
return BeatmapsStrings.PlayedUnplayed;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
[LocalisableEnum(typeof(SortCriteriaLocalisationMapper))]
|
||||
public enum SortCriteria
|
||||
{
|
||||
Title,
|
||||
@ -14,4 +19,40 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
Favourites,
|
||||
Relevance
|
||||
}
|
||||
|
||||
public class SortCriteriaLocalisationMapper : EnumLocalisationMapper<SortCriteria>
|
||||
{
|
||||
public override LocalisableString Map(SortCriteria value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case SortCriteria.Title:
|
||||
return BeatmapsStrings.ListingSearchSortingTitle;
|
||||
|
||||
case SortCriteria.Artist:
|
||||
return BeatmapsStrings.ListingSearchSortingArtist;
|
||||
|
||||
case SortCriteria.Difficulty:
|
||||
return BeatmapsStrings.ListingSearchSortingDifficulty;
|
||||
|
||||
case SortCriteria.Ranked:
|
||||
return BeatmapsStrings.ListingSearchSortingRanked;
|
||||
|
||||
case SortCriteria.Rating:
|
||||
return BeatmapsStrings.ListingSearchSortingRating;
|
||||
|
||||
case SortCriteria.Plays:
|
||||
return BeatmapsStrings.ListingSearchSortingPlays;
|
||||
|
||||
case SortCriteria.Favourites:
|
||||
return BeatmapsStrings.ListingSearchSortingFavourites;
|
||||
|
||||
case SortCriteria.Relevance:
|
||||
return BeatmapsStrings.ListingSearchSortingRelevance;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -22,8 +23,8 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
{
|
||||
private const float height = 50;
|
||||
|
||||
private readonly UpdateableAvatar avatar;
|
||||
private readonly FillFlowContainer fields;
|
||||
private UpdateableAvatar avatar;
|
||||
private FillFlowContainer fields;
|
||||
|
||||
private BeatmapSetInfo beatmapSet;
|
||||
|
||||
@ -35,11 +36,46 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
if (value == beatmapSet) return;
|
||||
|
||||
beatmapSet = value;
|
||||
|
||||
updateDisplay();
|
||||
Scheduler.AddOnce(updateDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = height;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
CornerRadius = 4,
|
||||
Masking = true,
|
||||
Child = avatar = new UpdateableAvatar(showGuestOnNull: false)
|
||||
{
|
||||
Size = new Vector2(height),
|
||||
},
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 4,
|
||||
Offset = new Vector2(0f, 1f),
|
||||
},
|
||||
},
|
||||
fields = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding { Left = height + 5 },
|
||||
},
|
||||
};
|
||||
|
||||
Scheduler.AddOnce(updateDisplay);
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
avatar.User = BeatmapSet?.Metadata.Author;
|
||||
@ -69,45 +105,6 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
}
|
||||
}
|
||||
|
||||
public AuthorInfo()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = height;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
CornerRadius = 4,
|
||||
Masking = true,
|
||||
Child = avatar = new UpdateableAvatar
|
||||
{
|
||||
ShowGuestOnNull = false,
|
||||
Size = new Vector2(height),
|
||||
},
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Colour = Color4.Black.Opacity(0.25f),
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 4,
|
||||
Offset = new Vector2(0f, 1f),
|
||||
},
|
||||
},
|
||||
fields = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding { Left = height + 5 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private void load()
|
||||
{
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
private class Field : FillFlowContainer
|
||||
{
|
||||
public Field(string first, string second, FontUsage secondFont)
|
||||
|
@ -61,7 +61,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
},
|
||||
}
|
||||
},
|
||||
avatar = new UpdateableAvatar
|
||||
avatar = new UpdateableAvatar(showGuestOnNull: false)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -75,7 +75,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
Offset = new Vector2(0, 2),
|
||||
Radius = 1,
|
||||
},
|
||||
ShowGuestOnNull = false,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
|
@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Chat.Tabs
|
||||
Child = new DelayedLoadWrapper(avatar = new ClickableAvatar(value.Users.First())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
OpenOnClick = { Value = false },
|
||||
OpenOnClick = false,
|
||||
})
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Bindables;
|
||||
@ -66,6 +68,8 @@ namespace osu.Game.Overlays.Comments
|
||||
public readonly BindableBool Checked = new BindableBool();
|
||||
|
||||
private readonly SpriteIcon checkboxIcon;
|
||||
private Sample sampleChecked;
|
||||
private Sample sampleUnchecked;
|
||||
|
||||
public ShowDeletedButton()
|
||||
{
|
||||
@ -93,6 +97,13 @@ namespace osu.Game.Overlays.Comments
|
||||
});
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
sampleChecked = audio.Samples.Get(@"UI/check-on");
|
||||
sampleUnchecked = audio.Samples.Get(@"UI/check-off");
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
Checked.BindValueChanged(isChecked => checkboxIcon.Icon = isChecked.NewValue ? FontAwesome.Solid.CheckSquare : FontAwesome.Regular.Square, true);
|
||||
@ -102,6 +113,12 @@ namespace osu.Game.Overlays.Comments
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
Checked.Value = !Checked.Value;
|
||||
|
||||
if (Checked.Value)
|
||||
sampleChecked?.Play();
|
||||
else
|
||||
sampleUnchecked?.Play();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays.Comments
|
||||
{
|
||||
@ -39,7 +38,6 @@ namespace osu.Game.Overlays.Comments
|
||||
Origin = Anchor.Centre,
|
||||
Margin = new MarginPadding { Horizontal = 10 }
|
||||
},
|
||||
new HoverClickSounds(),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
@ -13,6 +14,7 @@ using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -27,7 +29,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
public class KeyBindingRow : Container, IFilterable
|
||||
{
|
||||
private readonly object action;
|
||||
private readonly IEnumerable<Framework.Input.Bindings.KeyBinding> bindings;
|
||||
private readonly IEnumerable<RealmKeyBinding> bindings;
|
||||
|
||||
private const float transition_time = 150;
|
||||
|
||||
@ -62,7 +64,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
|
||||
public IEnumerable<string> FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend(text.Text.ToString());
|
||||
|
||||
public KeyBindingRow(object action, IEnumerable<Framework.Input.Bindings.KeyBinding> bindings)
|
||||
public KeyBindingRow(object action, List<RealmKeyBinding> bindings)
|
||||
{
|
||||
this.action = action;
|
||||
this.bindings = bindings;
|
||||
@ -72,7 +74,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private KeyBindingStore store { get; set; }
|
||||
private RealmContextFactory realmFactory { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
@ -153,7 +155,8 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
{
|
||||
var button = buttons[i++];
|
||||
button.UpdateKeyCombination(d);
|
||||
store.Update(button.KeyBinding);
|
||||
|
||||
updateStoreFromButton(button);
|
||||
}
|
||||
|
||||
isDefault.Value = true;
|
||||
@ -314,7 +317,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
{
|
||||
if (bindTarget != null)
|
||||
{
|
||||
store.Update(bindTarget.KeyBinding);
|
||||
updateStoreFromButton(bindTarget);
|
||||
|
||||
updateIsDefaultValue();
|
||||
|
||||
@ -361,6 +364,17 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
if (bindTarget != null) bindTarget.IsBinding = true;
|
||||
}
|
||||
|
||||
private void updateStoreFromButton(KeyButton button)
|
||||
{
|
||||
using (var usage = realmFactory.GetForWrite())
|
||||
{
|
||||
var binding = usage.Realm.Find<RealmKeyBinding>(((IHasGuidPrimaryKey)button.KeyBinding).ID);
|
||||
binding.KeyCombinationString = button.KeyBinding.KeyCombinationString;
|
||||
|
||||
usage.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateIsDefaultValue()
|
||||
{
|
||||
isDefault.Value = bindings.Select(b => b.KeyCombination).SequenceEqual(Defaults);
|
||||
@ -386,7 +400,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
|
||||
public class KeyButton : Container
|
||||
{
|
||||
public readonly Framework.Input.Bindings.KeyBinding KeyBinding;
|
||||
public readonly RealmKeyBinding KeyBinding;
|
||||
|
||||
private readonly Box box;
|
||||
public readonly OsuSpriteText Text;
|
||||
@ -408,8 +422,11 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
}
|
||||
}
|
||||
|
||||
public KeyButton(Framework.Input.Bindings.KeyBinding keyBinding)
|
||||
public KeyButton(RealmKeyBinding keyBinding)
|
||||
{
|
||||
if (keyBinding.IsManaged)
|
||||
throw new ArgumentException("Key binding should not be attached as we make temporary changes", nameof(keyBinding));
|
||||
|
||||
KeyBinding = keyBinding;
|
||||
|
||||
Margin = new MarginPadding(padding);
|
||||
@ -478,7 +495,7 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
|
||||
public void UpdateKeyCombination(KeyCombination newCombination)
|
||||
{
|
||||
if ((KeyBinding as DatabasedKeyBinding)?.RulesetID != null && !KeyBindingStore.CheckValidForGameplay(newCombination))
|
||||
if (KeyBinding.RulesetID != null && !RealmKeyBindingStore.CheckValidForGameplay(newCombination))
|
||||
return;
|
||||
|
||||
KeyBinding.KeyCombination = newCombination;
|
||||
|
@ -6,8 +6,9 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets;
|
||||
using osuTK;
|
||||
@ -31,16 +32,21 @@ namespace osu.Game.Overlays.KeyBinding
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(KeyBindingStore store)
|
||||
private void load(RealmContextFactory realmFactory)
|
||||
{
|
||||
var bindings = store.Query(Ruleset?.ID, variant);
|
||||
var rulesetId = Ruleset?.ID;
|
||||
|
||||
List<RealmKeyBinding> bindings;
|
||||
|
||||
using (var usage = realmFactory.GetForRead())
|
||||
bindings = usage.Realm.All<RealmKeyBinding>().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach();
|
||||
|
||||
foreach (var defaultGroup in Defaults.GroupBy(d => d.Action))
|
||||
{
|
||||
int intKey = (int)defaultGroup.Key;
|
||||
|
||||
// one row per valid action.
|
||||
Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => ((int)b.Action).Equals(intKey)))
|
||||
Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.ActionInt.Equals(intKey)).ToList())
|
||||
{
|
||||
AllowMainMouseButtons = Ruleset != null,
|
||||
Defaults = defaultGroup.Select(d => d.KeyCombination)
|
||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Mods
|
||||
base.OnModSelected(mod);
|
||||
|
||||
foreach (var section in ModSectionsContainer.Children)
|
||||
section.DeselectTypes(mod.IncompatibleMods, true);
|
||||
section.DeselectTypes(mod.IncompatibleMods, true, mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -302,7 +302,7 @@ namespace osu.Game.Overlays.Mods
|
||||
Anchor = Anchor.TopCentre,
|
||||
Font = OsuFont.GetFont(size: 18)
|
||||
},
|
||||
new HoverClickSounds(buttons: new[] { MouseButton.Left, MouseButton.Right })
|
||||
new HoverSounds()
|
||||
};
|
||||
|
||||
Mod = mod;
|
||||
|
@ -159,12 +159,16 @@ namespace osu.Game.Overlays.Mods
|
||||
/// </summary>
|
||||
/// <param name="modTypes">The types of <see cref="Mod"/>s which should be deselected.</param>
|
||||
/// <param name="immediate">Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow.</param>
|
||||
public void DeselectTypes(IEnumerable<Type> modTypes, bool immediate = false)
|
||||
/// <param name="newSelection">If this deselection is triggered by a user selection, this should contain the newly selected type. This type will never be deselected, even if it matches one provided in <paramref name="modTypes"/>.</param>
|
||||
public void DeselectTypes(IEnumerable<Type> modTypes, bool immediate = false, Mod newSelection = null)
|
||||
{
|
||||
foreach (var button in Buttons)
|
||||
{
|
||||
if (button.SelectedMod == null) continue;
|
||||
|
||||
if (button.SelectedMod == newSelection)
|
||||
continue;
|
||||
|
||||
foreach (var type in modTypes)
|
||||
{
|
||||
if (type.IsInstanceOfType(button.SelectedMod))
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -84,6 +85,7 @@ namespace osu.Game.Overlays
|
||||
private readonly Box background;
|
||||
|
||||
public ScrollToTopButton()
|
||||
: base(HoverSampleSet.ScrollToTop)
|
||||
{
|
||||
Size = new Vector2(50);
|
||||
Alpha = 0;
|
||||
|
@ -18,6 +18,7 @@ using JetBrains.Annotations;
|
||||
using System;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
@ -54,7 +55,7 @@ namespace osu.Game.Overlays
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
|
||||
Text = @"Sort by"
|
||||
Text = SortStrings.Default
|
||||
},
|
||||
CreateControl().With(c =>
|
||||
{
|
||||
@ -143,10 +144,12 @@ namespace osu.Game.Overlays
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
|
||||
Text = (value as Enum)?.GetDescription() ?? value.ToString()
|
||||
Text = (value as Enum)?.GetLocalisableDescription() ?? value.ToString()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddInternal(new HoverClickSounds());
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
@ -99,7 +99,7 @@ namespace osu.Game.Overlays
|
||||
ExpandedSize = 5f,
|
||||
CollapsedSize = 0
|
||||
},
|
||||
new HoverClickSounds()
|
||||
new HoverClickSounds(HoverSampleSet.TabSelect)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -58,13 +58,11 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
Origin = Anchor.CentreLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
avatar = new UpdateableAvatar
|
||||
avatar = new UpdateableAvatar(openOnClick: false, showGuestOnNull: false)
|
||||
{
|
||||
Size = new Vector2(avatar_size),
|
||||
Masking = true,
|
||||
CornerRadius = avatar_size * 0.25f,
|
||||
OpenOnClick = { Value = false },
|
||||
ShowGuestOnNull = false,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
|
@ -1,11 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections.General
|
||||
@ -35,11 +35,11 @@ namespace osu.Game.Overlays.Settings.Sections.General
|
||||
},
|
||||
};
|
||||
|
||||
if (!Enum.TryParse<Language>(frameworkLocale.Value, out var locale))
|
||||
if (!LanguageExtensions.TryParseCultureCode(frameworkLocale.Value, out var locale))
|
||||
locale = Language.en;
|
||||
languageSelection.Current.Value = locale;
|
||||
|
||||
languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToString());
|
||||
languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToCultureCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ namespace osu.Game.Overlays.Settings
|
||||
Margin = new MarginPadding { Top = 5 };
|
||||
RelativeSizeAxes = Axes.X;
|
||||
}
|
||||
|
||||
protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +106,19 @@ namespace osu.Game.Overlays
|
||||
public OverlayHeaderTabItem(T value)
|
||||
: base(value)
|
||||
{
|
||||
Text.Text = ((Value as Enum)?.GetDescription() ?? Value.ToString()).ToLower();
|
||||
if (!(Value is Enum enumValue))
|
||||
Text.Text = Value.ToString().ToLower();
|
||||
else
|
||||
{
|
||||
var localisableDescription = enumValue.GetLocalisableDescription();
|
||||
var nonLocalisableDescription = enumValue.GetDescription();
|
||||
|
||||
// If localisable == non-localisable, then we must have a basic string, so .ToLower() is used.
|
||||
Text.Text = localisableDescription.Equals(nonLocalisableDescription)
|
||||
? nonLocalisableDescription.ToLower()
|
||||
: localisableDescription;
|
||||
}
|
||||
|
||||
Text.Font = OsuFont.GetFont(size: 14);
|
||||
Text.Margin = new MarginPadding { Vertical = 16.5f }; // 15px padding + 1.5px line-height difference compensation
|
||||
Bar.Margin = new MarginPadding { Bottom = bar_height };
|
||||
|
@ -1,8 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -13,13 +13,13 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Database;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -76,7 +76,7 @@ namespace osu.Game.Overlays.Toolbar
|
||||
protected FillFlowContainer Flow;
|
||||
|
||||
[Resolved]
|
||||
private KeyBindingStore keyBindings { get; set; }
|
||||
private RealmContextFactory realmFactory { get; set; }
|
||||
|
||||
protected ToolbarButton()
|
||||
: base(HoverSampleSet.Toolbar)
|
||||
@ -159,27 +159,28 @@ namespace osu.Game.Overlays.Toolbar
|
||||
};
|
||||
}
|
||||
|
||||
private readonly Cached tooltipKeyBinding = new Cached();
|
||||
private RealmKeyBinding realmKeyBinding;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
keyBindings.KeyBindingChanged += () => tooltipKeyBinding.Invalidate();
|
||||
base.LoadComplete();
|
||||
|
||||
if (Hotkey == null) return;
|
||||
|
||||
realmKeyBinding = realmFactory.Context.All<RealmKeyBinding>().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value);
|
||||
|
||||
if (realmKeyBinding != null)
|
||||
{
|
||||
realmKeyBinding.PropertyChanged += (sender, args) =>
|
||||
{
|
||||
if (args.PropertyName == nameof(realmKeyBinding.KeyCombinationString))
|
||||
updateKeyBindingTooltip();
|
||||
};
|
||||
}
|
||||
|
||||
updateKeyBindingTooltip();
|
||||
}
|
||||
|
||||
private void updateKeyBindingTooltip()
|
||||
{
|
||||
if (tooltipKeyBinding.IsValid)
|
||||
return;
|
||||
|
||||
var binding = keyBindings.Query().Find(b => (GlobalAction)b.Action == Hotkey);
|
||||
var keyBindingString = binding?.KeyCombination.ReadableString();
|
||||
keyBindingTooltip.Text = !string.IsNullOrEmpty(keyBindingString) ? $" ({keyBindingString})" : string.Empty;
|
||||
|
||||
tooltipKeyBinding.Validate();
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
@ -218,6 +219,17 @@ namespace osu.Game.Overlays.Toolbar
|
||||
public void OnReleased(GlobalAction action)
|
||||
{
|
||||
}
|
||||
|
||||
private void updateKeyBindingTooltip()
|
||||
{
|
||||
if (realmKeyBinding != null)
|
||||
{
|
||||
var keyBindingString = realmKeyBinding.KeyCombination.ReadableString();
|
||||
|
||||
if (!string.IsNullOrEmpty(keyBindingString))
|
||||
keyBindingTooltip.Text = $" ({keyBindingString})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class OpaqueBackground : Container
|
||||
|
@ -32,14 +32,13 @@ namespace osu.Game.Overlays.Toolbar
|
||||
|
||||
Add(new OpaqueBackground { Depth = 1 });
|
||||
|
||||
Flow.Add(avatar = new UpdateableAvatar
|
||||
Flow.Add(avatar = new UpdateableAvatar(openOnClick: false)
|
||||
{
|
||||
Masking = true,
|
||||
Size = new Vector2(32),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
CornerRadius = 4,
|
||||
OpenOnClick = { Value = false },
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
|
41
osu.Game/Performance/HighPerformanceSession.cs
Normal file
41
osu.Game/Performance/HighPerformanceSession.cs
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
|
||||
namespace osu.Game.Performance
|
||||
{
|
||||
public class HighPerformanceSession : Component
|
||||
{
|
||||
private readonly IBindable<bool> localUserPlaying = new Bindable<bool>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuGame game)
|
||||
{
|
||||
localUserPlaying.BindTo(game.LocalUserPlaying);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
localUserPlaying.BindValueChanged(playing =>
|
||||
{
|
||||
if (playing.NewValue)
|
||||
EnableHighPerformanceSession();
|
||||
else
|
||||
DisableHighPerformanceSession();
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected virtual void EnableHighPerformanceSession()
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void DisableHighPerformanceSession()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -43,6 +43,9 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
protected readonly Ruleset Ruleset;
|
||||
|
||||
// Provides `Playfield`
|
||||
private DependencyContainer dependencies;
|
||||
|
||||
[Resolved]
|
||||
protected EditorClock EditorClock { get; private set; }
|
||||
|
||||
@ -69,6 +72,9 @@ namespace osu.Game.Rulesets.Edit
|
||||
Ruleset = ruleset;
|
||||
}
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
|
||||
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@ -88,6 +94,8 @@ namespace osu.Game.Rulesets.Edit
|
||||
return;
|
||||
}
|
||||
|
||||
dependencies.CacheAs(Playfield);
|
||||
|
||||
const float toolbar_width = 200;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
|
@ -1,7 +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.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
@ -9,13 +8,12 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// <summary>
|
||||
/// An interface for <see cref="Mod"/>s that can be applied to <see cref="DrawableHitObject"/>s.
|
||||
/// </summary>
|
||||
public interface IApplicableToDrawableHitObjects : IApplicableMod
|
||||
public interface IApplicableToDrawableHitObject : IApplicableMod
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies this <see cref="IApplicableToDrawableHitObjects"/> to a list of <see cref="DrawableHitObject"/>s.
|
||||
/// Applies this <see cref="IApplicableToDrawableHitObject"/> to a <see cref="DrawableHitObject"/>.
|
||||
/// This will only be invoked with top-level <see cref="DrawableHitObject"/>s. Access <see cref="DrawableHitObject.NestedHitObjects"/> if adjusting nested objects is necessary.
|
||||
/// </summary>
|
||||
/// <param name="drawables">The list of <see cref="DrawableHitObject"/>s to apply to.</param>
|
||||
void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables);
|
||||
void ApplyToDrawableHitObject(DrawableHitObject drawable);
|
||||
}
|
||||
}
|
||||
|
18
osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs
Normal file
18
osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.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 System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
[Obsolete(@"Use the singular version IApplicableToDrawableHitObject instead.")] // Can be removed 20211216
|
||||
public interface IApplicableToDrawableHitObjects : IApplicableToDrawableHitObject
|
||||
{
|
||||
void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables);
|
||||
|
||||
void IApplicableToDrawableHitObject.ApplyToDrawableHitObject(DrawableHitObject drawable) => ApplyToDrawableHitObjects(drawable.Yield());
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// A <see cref="Mod"/> which applies visibility adjustments to <see cref="DrawableHitObject"/>s
|
||||
/// with an optional increased visibility adjustment depending on the user's "increase first object visibility" setting.
|
||||
/// </summary>
|
||||
public abstract class ModWithVisibilityAdjustment : Mod, IReadFromConfig, IApplicableToBeatmap, IApplicableToDrawableHitObjects
|
||||
public abstract class ModWithVisibilityAdjustment : Mod, IReadFromConfig, IApplicableToBeatmap, IApplicableToDrawableHitObject
|
||||
{
|
||||
/// <summary>
|
||||
/// The first adjustable object.
|
||||
@ -73,19 +73,16 @@ namespace osu.Game.Rulesets.Mods
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
|
||||
public virtual void ApplyToDrawableHitObject(DrawableHitObject dho)
|
||||
{
|
||||
foreach (var dho in drawables)
|
||||
dho.ApplyCustomUpdateState += (o, state) =>
|
||||
{
|
||||
dho.ApplyCustomUpdateState += (o, state) =>
|
||||
{
|
||||
// Increased visibility is applied to the entire first object, including all of its nested hitobjects.
|
||||
if (IncreaseFirstObjectVisibility.Value && isObjectEqualToOrNestedIn(o.HitObject, FirstObject))
|
||||
ApplyIncreasedVisibilityState(o, state);
|
||||
else
|
||||
ApplyNormalVisibilityState(o, state);
|
||||
};
|
||||
}
|
||||
// Increased visibility is applied to the entire first object, including all of its nested hitobjects.
|
||||
if (IncreaseFirstObjectVisibility.Value && isObjectEqualToOrNestedIn(o.HitObject, FirstObject))
|
||||
ApplyIncreasedVisibilityState(o, state);
|
||||
else
|
||||
ApplyNormalVisibilityState(o, state);
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -156,10 +156,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// If <c>null</c>, a hitobject is expected to be later applied via <see cref="PoolableDrawableWithLifetime{TEntry}.Apply"/> (or automatically via pooling).
|
||||
/// </param>
|
||||
protected DrawableHitObject([CanBeNull] HitObject initialHitObject = null)
|
||||
: base(initialHitObject != null ? new SyntheticHitObjectEntry(initialHitObject) : null)
|
||||
{
|
||||
if (Entry != null)
|
||||
ensureEntryHasResult();
|
||||
if (initialHitObject == null) return;
|
||||
|
||||
Entry = new SyntheticHitObjectEntry(initialHitObject);
|
||||
ensureEntryHasResult();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
|
||||
@ -16,14 +17,32 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
/// <typeparam name="TEntry">The <see cref="LifetimeEntry"/> type storing state and controlling this drawable.</typeparam>
|
||||
public abstract class PoolableDrawableWithLifetime<TEntry> : PoolableDrawable where TEntry : LifetimeEntry
|
||||
{
|
||||
private TEntry? entry;
|
||||
|
||||
/// <summary>
|
||||
/// The entry holding essential state of this <see cref="PoolableDrawableWithLifetime{TEntry}"/>.
|
||||
/// </summary>
|
||||
public TEntry? Entry { get; private set; }
|
||||
/// <remarks>
|
||||
/// If a non-null value is set before loading is started, the entry is applied when the loading is completed.
|
||||
/// It is not valid to set an entry while this <see cref="PoolableDrawableWithLifetime{TEntry}"/> is loading.
|
||||
/// </remarks>
|
||||
public TEntry? Entry
|
||||
{
|
||||
get => entry;
|
||||
set
|
||||
{
|
||||
if (LoadState == LoadState.NotLoaded)
|
||||
entry = value;
|
||||
else if (value != null)
|
||||
Apply(value);
|
||||
else if (HasEntryApplied)
|
||||
free();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="Entry"/> is applied to this <see cref="PoolableDrawableWithLifetime{TEntry}"/>.
|
||||
/// When an initial entry is specified in the constructor, <see cref="Entry"/> is set but not applied until loading is completed.
|
||||
/// When an <see cref="Entry"/> is set during initialization, it is not applied until loading is completed.
|
||||
/// </summary>
|
||||
protected bool HasEntryApplied { get; private set; }
|
||||
|
||||
@ -65,9 +84,9 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
{
|
||||
base.LoadAsyncComplete();
|
||||
|
||||
// Apply the initial entry given in the constructor.
|
||||
// Apply the initial entry.
|
||||
if (Entry != null && !HasEntryApplied)
|
||||
Apply(Entry);
|
||||
apply(Entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -76,16 +95,10 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
/// </summary>
|
||||
public void Apply(TEntry entry)
|
||||
{
|
||||
if (HasEntryApplied)
|
||||
free();
|
||||
if (LoadState == LoadState.Loading)
|
||||
throw new InvalidOperationException($"Cannot apply a new {nameof(TEntry)} while currently loading.");
|
||||
|
||||
Entry = entry;
|
||||
entry.LifetimeChanged += setLifetimeFromEntry;
|
||||
setLifetimeFromEntry(entry);
|
||||
|
||||
OnApply(entry);
|
||||
|
||||
HasEntryApplied = true;
|
||||
apply(entry);
|
||||
}
|
||||
|
||||
protected sealed override void FreeAfterUse()
|
||||
@ -111,6 +124,20 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
{
|
||||
}
|
||||
|
||||
private void apply(TEntry entry)
|
||||
{
|
||||
if (HasEntryApplied)
|
||||
free();
|
||||
|
||||
this.entry = entry;
|
||||
entry.LifetimeChanged += setLifetimeFromEntry;
|
||||
setLifetimeFromEntry(entry);
|
||||
|
||||
OnApply(entry);
|
||||
|
||||
HasEntryApplied = true;
|
||||
}
|
||||
|
||||
private void free()
|
||||
{
|
||||
Debug.Assert(Entry != null && HasEntryApplied);
|
||||
@ -118,7 +145,7 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
OnFree(Entry);
|
||||
|
||||
Entry.LifetimeChanged -= setLifetimeFromEntry;
|
||||
Entry = null;
|
||||
entry = null;
|
||||
base.LifetimeStart = double.MinValue;
|
||||
base.LifetimeEnd = double.MaxValue;
|
||||
|
||||
|
@ -6,7 +6,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Game.Input.Handlers;
|
||||
using osu.Game.Replays;
|
||||
|
||||
@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Replays
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool IsImportant([NotNull] TFrame frame) => false;
|
||||
protected virtual bool IsImportant(TFrame frame) => false;
|
||||
|
||||
/// <summary>
|
||||
/// Update the current frame based on an incoming time value.
|
||||
|
@ -96,13 +96,25 @@ namespace osu.Game.Rulesets
|
||||
|
||||
context.SaveChanges();
|
||||
|
||||
// add any other modes
|
||||
var existingRulesets = context.RulesetInfo.ToList();
|
||||
|
||||
// add any other rulesets which have assemblies present but are not yet in the database.
|
||||
foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
|
||||
{
|
||||
if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
|
||||
context.RulesetInfo.Add(r.RulesetInfo);
|
||||
{
|
||||
var existingSameShortName = existingRulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
|
||||
|
||||
if (existingSameShortName != null)
|
||||
{
|
||||
// even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
|
||||
// this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
|
||||
// in such cases, update the instantiation info of the existing entry to point to the new one.
|
||||
existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
|
||||
}
|
||||
else
|
||||
context.RulesetInfo.Add(r.RulesetInfo);
|
||||
}
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
|
@ -199,8 +199,11 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
Playfield.PostProcess();
|
||||
|
||||
foreach (var mod in Mods.OfType<IApplicableToDrawableHitObjects>())
|
||||
mod.ApplyToDrawableHitObjects(Playfield.AllHitObjects);
|
||||
foreach (var mod in Mods.OfType<IApplicableToDrawableHitObject>())
|
||||
{
|
||||
foreach (var drawableHitObject in Playfield.AllHitObjects)
|
||||
mod.ApplyToDrawableHitObject(drawableHitObject);
|
||||
}
|
||||
}
|
||||
|
||||
public override void RequestResume(Action continueResume)
|
||||
|
@ -356,8 +356,8 @@ namespace osu.Game.Rulesets.UI
|
||||
// This is done before Apply() so that the state is updated once when the hitobject is applied.
|
||||
if (mods != null)
|
||||
{
|
||||
foreach (var m in mods.OfType<IApplicableToDrawableHitObjects>())
|
||||
m.ApplyToDrawableHitObjects(dho.Yield());
|
||||
foreach (var m in mods.OfType<IApplicableToDrawableHitObject>())
|
||||
m.ApplyToDrawableHitObject(dho);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
base.ReloadMappings();
|
||||
|
||||
KeyBindings = KeyBindings.Where(b => KeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
|
||||
KeyBindings = KeyBindings.Where(b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
/// </remarks>
|
||||
public double TimeAtPosition(float localPosition, double currentTime)
|
||||
{
|
||||
float scrollPosition = axisInverted ? scrollLength - localPosition : localPosition;
|
||||
float scrollPosition = axisInverted ? -localPosition : localPosition;
|
||||
return scrollingInfo.Algorithm.TimeAt(scrollPosition, currentTime, timeRange.Value, scrollLength);
|
||||
}
|
||||
|
||||
@ -81,8 +81,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
/// </remarks>
|
||||
public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition)
|
||||
{
|
||||
Vector2 localPosition = ToLocalSpace(screenSpacePosition);
|
||||
return TimeAtPosition(scrollingAxis == Direction.Horizontal ? localPosition.X : localPosition.Y, Time.Current);
|
||||
Vector2 pos = ToLocalSpace(screenSpacePosition);
|
||||
float localPosition = scrollingAxis == Direction.Horizontal ? pos.X : pos.Y;
|
||||
localPosition -= axisInverted ? scrollLength : 0;
|
||||
return TimeAtPosition(localPosition, Time.Current);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -91,7 +93,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
public float PositionAtTime(double time, double currentTime)
|
||||
{
|
||||
float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength);
|
||||
return axisInverted ? scrollLength - scrollPosition : scrollPosition;
|
||||
return axisInverted ? -scrollPosition : scrollPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -106,6 +108,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
public Vector2 ScreenSpacePositionAtTime(double time)
|
||||
{
|
||||
float localPosition = PositionAtTime(time, Time.Current);
|
||||
localPosition += axisInverted ? scrollLength : 0;
|
||||
return scrollingAxis == Direction.Horizontal
|
||||
? ToScreenSpace(new Vector2(localPosition, DrawHeight / 2))
|
||||
: ToScreenSpace(new Vector2(DrawWidth / 2, localPosition));
|
||||
@ -236,14 +239,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
{
|
||||
float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime);
|
||||
|
||||
// The position returned from `PositionAtTime` is assuming the `TopLeft` anchor.
|
||||
// A correction is needed because the hit objects are using a different anchor for each direction (e.g. `BottomCentre` for `Bottom` direction).
|
||||
float anchorCorrection = axisInverted ? scrollLength : 0;
|
||||
|
||||
if (scrollingAxis == Direction.Horizontal)
|
||||
hitObject.X = position - anchorCorrection;
|
||||
hitObject.X = position;
|
||||
else
|
||||
hitObject.Y = position - anchorCorrection;
|
||||
hitObject.Y = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Scoring
|
||||
{
|
||||
[LocalisableEnum(typeof(ScoreRankEnumLocalisationMapper))]
|
||||
public enum ScoreRank
|
||||
{
|
||||
[Description(@"D")]
|
||||
@ -31,4 +35,40 @@ namespace osu.Game.Scoring
|
||||
[Description(@"SS+")]
|
||||
XH,
|
||||
}
|
||||
|
||||
public class ScoreRankEnumLocalisationMapper : EnumLocalisationMapper<ScoreRank>
|
||||
{
|
||||
public override LocalisableString Map(ScoreRank value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case ScoreRank.XH:
|
||||
return BeatmapsStrings.RankXH;
|
||||
|
||||
case ScoreRank.X:
|
||||
return BeatmapsStrings.RankX;
|
||||
|
||||
case ScoreRank.SH:
|
||||
return BeatmapsStrings.RankSH;
|
||||
|
||||
case ScoreRank.S:
|
||||
return BeatmapsStrings.RankS;
|
||||
|
||||
case ScoreRank.A:
|
||||
return BeatmapsStrings.RankA;
|
||||
|
||||
case ScoreRank.B:
|
||||
return BeatmapsStrings.RankB;
|
||||
|
||||
case ScoreRank.C:
|
||||
return BeatmapsStrings.RankC;
|
||||
|
||||
case ScoreRank.D:
|
||||
return BeatmapsStrings.RankD;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Users;
|
||||
@ -91,7 +90,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
||||
});
|
||||
}
|
||||
|
||||
private class UserTile : CompositeDrawable, IHasTooltip
|
||||
private class UserTile : CompositeDrawable
|
||||
{
|
||||
public User User
|
||||
{
|
||||
@ -99,8 +98,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
||||
set => avatar.User = value;
|
||||
}
|
||||
|
||||
public string TooltipText => User?.Username ?? string.Empty;
|
||||
|
||||
private readonly UpdateableAvatar avatar;
|
||||
|
||||
public UserTile()
|
||||
@ -116,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex(@"27252d"),
|
||||
},
|
||||
avatar = new UpdateableAvatar { RelativeSizeAxes = Axes.Both },
|
||||
avatar = new UpdateableAvatar(showUsernameTooltip: true) { RelativeSizeAxes = Axes.Both },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Screens;
|
||||
@ -54,9 +55,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true);
|
||||
}
|
||||
|
||||
protected override void PrepareScoreForResults()
|
||||
protected override async Task PrepareScoreForResultsAsync(Score score)
|
||||
{
|
||||
base.PrepareScoreForResults();
|
||||
await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
|
||||
|
||||
Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
|
||||
}
|
||||
|
@ -181,12 +181,6 @@ namespace osu.Game.Screens.Play
|
||||
DrawableRuleset.SetRecordTarget(Score);
|
||||
}
|
||||
|
||||
protected virtual void PrepareScoreForResults()
|
||||
{
|
||||
// perform one final population to ensure everything is up-to-date.
|
||||
ScoreProcessor.PopulateScore(Score.ScoreInfo);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game)
|
||||
{
|
||||
@ -295,12 +289,12 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
|
||||
{
|
||||
if (storyboardEnded.NewValue && completionProgressDelegate == null)
|
||||
updateCompletionState();
|
||||
if (storyboardEnded.NewValue)
|
||||
progressToResults(true);
|
||||
};
|
||||
|
||||
// Bind the judgement processors to ourselves
|
||||
ScoreProcessor.HasCompleted.BindValueChanged(_ => updateCompletionState());
|
||||
ScoreProcessor.HasCompleted.BindValueChanged(scoreCompletionChanged);
|
||||
HealthProcessor.Failed += onFail;
|
||||
|
||||
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
|
||||
@ -376,7 +370,7 @@ namespace osu.Game.Screens.Play
|
||||
},
|
||||
skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0)
|
||||
{
|
||||
RequestSkip = () => updateCompletionState(true),
|
||||
RequestSkip = () => progressToResults(false),
|
||||
Alpha = 0
|
||||
},
|
||||
FailOverlay = new FailOverlay
|
||||
@ -508,19 +502,25 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exits the <see cref="Player"/>.
|
||||
/// Attempts to complete a user request to exit gameplay.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item>This should only be called in response to a user interaction. Exiting is not guaranteed.</item>
|
||||
/// <item>This will interrupt any pending progression to the results screen, even if the transition has begun.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <param name="showDialogFirst">
|
||||
/// Whether the pause or fail dialog should be shown before performing an exit.
|
||||
/// If true and a dialog is not yet displayed, the exit will be blocked the the relevant dialog will display instead.
|
||||
/// If <see langword="true"/> and a dialog is not yet displayed, the exit will be blocked and the relevant dialog will display instead.
|
||||
/// </param>
|
||||
protected void PerformExit(bool showDialogFirst)
|
||||
{
|
||||
// if a restart has been requested, cancel any pending completion (user has shown intent to restart).
|
||||
completionProgressDelegate?.Cancel();
|
||||
// if an exit has been requested, cancel any pending completion (the user has shown intention to exit).
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
// there is a chance that the exit was performed after the transition to results has started.
|
||||
// we want to give the user what they want, so forcefully return to this screen (to proceed with the upwards exit process).
|
||||
// there is a chance that an exit request occurs after the transition to results has already started.
|
||||
// even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process).
|
||||
if (!this.IsCurrentScreen())
|
||||
{
|
||||
ValidForResume = false;
|
||||
@ -543,7 +543,7 @@ namespace osu.Game.Screens.Play
|
||||
return;
|
||||
}
|
||||
|
||||
// there's a chance the pausing is not supported in the current state, at which point immediate exit should be preferred.
|
||||
// even if this call has requested a dialog, there is a chance the current player mode doesn't support pausing.
|
||||
if (pausingSupportedByCurrentState)
|
||||
{
|
||||
// in the case a dialog needs to be shown, attempt to pause and show it.
|
||||
@ -551,14 +551,12 @@ namespace osu.Game.Screens.Play
|
||||
Pause();
|
||||
return;
|
||||
}
|
||||
|
||||
// if the score is ready for display but results screen has not been pushed yet (e.g. storyboard is still playing beyond gameplay), then transition to results screen instead of exiting.
|
||||
if (prepareScoreForDisplayTask != null && completionProgressDelegate == null)
|
||||
{
|
||||
updateCompletionState(true);
|
||||
}
|
||||
}
|
||||
|
||||
// The actual exit is performed if
|
||||
// - the pause / fail dialog was not requested
|
||||
// - the pause / fail dialog was requested but is already displayed (user showing intention to exit).
|
||||
// - the pause / fail dialog was requested but couldn't be displayed due to the type or state of this Player instance.
|
||||
this.Exit();
|
||||
}
|
||||
|
||||
@ -622,98 +620,141 @@ namespace osu.Game.Screens.Play
|
||||
PerformExit(false);
|
||||
}
|
||||
|
||||
private ScheduledDelegate completionProgressDelegate;
|
||||
/// <summary>
|
||||
/// This delegate, when set, means the results screen has been queued to appear.
|
||||
/// The display of the results screen may be delayed by any work being done in <see cref="PrepareScoreForResultsAsync"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Once set, this can *only* be cancelled by rewinding, ie. if <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="false"/>.
|
||||
/// Even if the user requests an exit, it will forcefully proceed to the results screen (see special case in <see cref="OnExiting"/>).
|
||||
/// </remarks>
|
||||
private ScheduledDelegate resultsDisplayDelegate;
|
||||
|
||||
/// <summary>
|
||||
/// A task which asynchronously prepares a completed score for display at results.
|
||||
/// This may include performing net requests or importing the score into the database, generally to ensure things are in a sane state for the play session.
|
||||
/// </summary>
|
||||
private Task<ScoreInfo> prepareScoreForDisplayTask;
|
||||
|
||||
/// <summary>
|
||||
/// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime.
|
||||
/// </summary>
|
||||
/// <param name="skipStoryboardOutro">If in a state where a storyboard outro is to be played, offers the choice of skipping beyond it.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown if this method is called more than once without changing state.</exception>
|
||||
private void updateCompletionState(bool skipStoryboardOutro = false)
|
||||
private void scoreCompletionChanged(ValueChangedEvent<bool> completed)
|
||||
{
|
||||
// screen may be in the exiting transition phase.
|
||||
// If this player instance is in the middle of an exit, don't attempt any kind of state update.
|
||||
if (!this.IsCurrentScreen())
|
||||
return;
|
||||
|
||||
if (!ScoreProcessor.HasCompleted.Value)
|
||||
// Special case to handle rewinding post-completion. This is the only way already queued forward progress can be cancelled.
|
||||
// TODO: Investigate whether this can be moved to a RewindablePlayer subclass or similar.
|
||||
// Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run).
|
||||
// In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done,
|
||||
// but it still doesn't feel right that this exists here.
|
||||
if (!completed.NewValue)
|
||||
{
|
||||
completionProgressDelegate?.Cancel();
|
||||
completionProgressDelegate = null;
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
resultsDisplayDelegate = null;
|
||||
|
||||
ValidForResume = true;
|
||||
skipOutroOverlay.Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (completionProgressDelegate != null)
|
||||
throw new InvalidOperationException($"{nameof(updateCompletionState)} was fired more than once");
|
||||
|
||||
// Only show the completion screen if the player hasn't failed
|
||||
if (HealthProcessor.HasFailed)
|
||||
return;
|
||||
|
||||
// Setting this early in the process means that even if something were to go wrong in the order of events following, there
|
||||
// is no chance that a user could return to the (already completed) Player instance from a child screen.
|
||||
ValidForResume = false;
|
||||
|
||||
// ensure we are not writing to the replay any more, as we are about to consume and store the score.
|
||||
// Ensure we are not writing to the replay any more, as we are about to consume and store the score.
|
||||
DrawableRuleset.SetRecordTarget(null);
|
||||
|
||||
if (!Configuration.ShowResults) return;
|
||||
|
||||
prepareScoreForDisplayTask ??= Task.Run(async () =>
|
||||
{
|
||||
PrepareScoreForResults();
|
||||
|
||||
try
|
||||
{
|
||||
await PrepareScoreForResultsAsync(Score).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, "Score preparation failed!");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await ImportScore(Score).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, "Score import failed!");
|
||||
}
|
||||
|
||||
return Score.ScoreInfo;
|
||||
});
|
||||
|
||||
if (skipStoryboardOutro)
|
||||
{
|
||||
scheduleCompletion();
|
||||
if (!Configuration.ShowResults)
|
||||
return;
|
||||
}
|
||||
|
||||
prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults);
|
||||
|
||||
bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
|
||||
|
||||
if (storyboardHasOutro)
|
||||
{
|
||||
// if the current beatmap has a storyboard, the progression to results will be handled by the storyboard ending
|
||||
// or the user pressing the skip outro button.
|
||||
skipOutroOverlay.Show();
|
||||
return;
|
||||
}
|
||||
|
||||
using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
|
||||
scheduleCompletion();
|
||||
progressToResults(true);
|
||||
}
|
||||
|
||||
private void scheduleCompletion() => completionProgressDelegate = Schedule(() =>
|
||||
/// <summary>
|
||||
/// Asynchronously run score preparation operations (database import, online submission etc.).
|
||||
/// </summary>
|
||||
/// <returns>The final score.</returns>
|
||||
private async Task<ScoreInfo> prepareScoreForResults()
|
||||
{
|
||||
if (!prepareScoreForDisplayTask.IsCompleted)
|
||||
try
|
||||
{
|
||||
scheduleCompletion();
|
||||
return;
|
||||
await PrepareScoreForResultsAsync(Score).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, @"Score preparation failed!");
|
||||
}
|
||||
|
||||
// screen may be in the exiting transition phase.
|
||||
if (this.IsCurrentScreen())
|
||||
try
|
||||
{
|
||||
await ImportScore(Score).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, @"Score import failed!");
|
||||
}
|
||||
|
||||
return Score.ScoreInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue the results screen for display.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A final display will only occur once all work is completed in <see cref="PrepareScoreForResultsAsync"/>. This means that even after calling this method, the results screen will never be shown until <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="true"/>.
|
||||
///
|
||||
/// Calling this method multiple times will have no effect.
|
||||
/// </remarks>
|
||||
/// <param name="withDelay">Whether a minimum delay (<see cref="RESULTS_DISPLAY_DELAY"/>) should be added before the screen is displayed.</param>
|
||||
private void progressToResults(bool withDelay)
|
||||
{
|
||||
if (resultsDisplayDelegate != null)
|
||||
// Note that if progressToResults is called one withDelay=true and then withDelay=false, this no-delay timing will not be
|
||||
// accounted for. shouldn't be a huge concern (a user pressing the skip button after a results progression has already been queued
|
||||
// may take x00 more milliseconds than expected in the very rare edge case).
|
||||
//
|
||||
// If required we can handle this more correctly by rescheduling here.
|
||||
return;
|
||||
|
||||
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0;
|
||||
|
||||
resultsDisplayDelegate = new ScheduledDelegate(() =>
|
||||
{
|
||||
if (prepareScoreForDisplayTask?.IsCompleted != true)
|
||||
// If the asynchronous preparation has not completed, keep repeating this delegate.
|
||||
return;
|
||||
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
if (!this.IsCurrentScreen())
|
||||
// This player instance may already be in the process of exiting.
|
||||
return;
|
||||
|
||||
this.Push(CreateResults(prepareScoreForDisplayTask.Result));
|
||||
});
|
||||
}, Time.Current + delay, 50);
|
||||
|
||||
Scheduler.Add(resultsDisplayDelegate);
|
||||
}
|
||||
|
||||
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
|
||||
|
||||
@ -911,13 +952,6 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
screenSuspension?.Expire();
|
||||
|
||||
if (completionProgressDelegate != null && !completionProgressDelegate.Cancelled && !completionProgressDelegate.Completed)
|
||||
{
|
||||
// proceed to result screen if beatmap already finished playing
|
||||
completionProgressDelegate.RunTask();
|
||||
return true;
|
||||
}
|
||||
|
||||
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
|
||||
// To resolve test failures, forcefully end playing synchronously when this screen exits.
|
||||
// Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method.
|
||||
@ -980,7 +1014,13 @@ namespace osu.Game.Screens.Play
|
||||
/// </summary>
|
||||
/// <param name="score">The <see cref="Scoring.Score"/> to prepare.</param>
|
||||
/// <returns>A task that prepares the provided score. On completion, the score is assumed to be ready for display.</returns>
|
||||
protected virtual Task PrepareScoreForResultsAsync(Score score) => Task.CompletedTask;
|
||||
protected virtual Task PrepareScoreForResultsAsync(Score score)
|
||||
{
|
||||
// perform one final population to ensure everything is up-to-date.
|
||||
ScoreProcessor.PopulateScore(score.ScoreInfo);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="ResultsScreen"/> for a <see cref="ScoreInfo"/>.
|
||||
|
@ -66,7 +66,7 @@ namespace osu.Game.Screens.Select
|
||||
private readonly Box light;
|
||||
|
||||
public FooterButton()
|
||||
: base(HoverSampleSet.SongSelect)
|
||||
: base(HoverSampleSet.Button)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Shear = SHEAR;
|
||||
|
@ -18,6 +18,8 @@ namespace osu.Game.Skinning
|
||||
|
||||
private readonly BindableList<ISkinnableDrawable> components = new BindableList<ISkinnableDrawable>();
|
||||
|
||||
public bool ComponentsLoaded { get; private set; }
|
||||
|
||||
public SkinnableTargetContainer(SkinnableTarget target)
|
||||
{
|
||||
Target = target;
|
||||
@ -30,6 +32,7 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
ClearInternal();
|
||||
components.Clear();
|
||||
ComponentsLoaded = false;
|
||||
|
||||
content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetComponentsContainer;
|
||||
|
||||
@ -39,8 +42,11 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
AddInternal(wrapper);
|
||||
components.AddRange(wrapper.Children.OfType<ISkinnableDrawable>());
|
||||
ComponentsLoaded = true;
|
||||
});
|
||||
}
|
||||
else
|
||||
ComponentsLoaded = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISkinnableTarget"/>
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Storyboards.Drawables
|
||||
/// </summary>
|
||||
public IBindable<bool> HasStoryboardEnded => hasStoryboardEnded;
|
||||
|
||||
private readonly BindableBool hasStoryboardEnded = new BindableBool();
|
||||
private readonly BindableBool hasStoryboardEnded = new BindableBool(true);
|
||||
|
||||
protected override Container<DrawableStoryboardLayer> Content { get; }
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
@ -43,6 +44,8 @@ namespace osu.Game.Tests.Visual
|
||||
LegacySkin.ResetDrawableTarget(t);
|
||||
t.Reload();
|
||||
}));
|
||||
|
||||
AddUntilStep("wait for components to load", () => this.ChildrenOfType<SkinnableTargetContainer>().All(t => t.ComponentsLoaded));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -350,7 +350,7 @@ namespace osu.Game.Tests.Visual
|
||||
if (CurrentTime >= Length)
|
||||
{
|
||||
Stop();
|
||||
RaiseCompleted();
|
||||
// `RaiseCompleted` is not called here to prevent transitioning to the next song.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
@ -13,16 +12,32 @@ namespace osu.Game.Users.Drawables
|
||||
{
|
||||
public class ClickableAvatar : Container
|
||||
{
|
||||
private const string default_tooltip_text = "view profile";
|
||||
|
||||
/// <summary>
|
||||
/// Whether to open the user's profile when clicked.
|
||||
/// </summary>
|
||||
public readonly BindableBool OpenOnClick = new BindableBool(true);
|
||||
public bool OpenOnClick
|
||||
{
|
||||
set => clickableArea.Enabled.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// By default, the tooltip will show "view profile" as avatars are usually displayed next to a username.
|
||||
/// Setting this to <c>true</c> exposes the username via tooltip for special cases where this is not true.
|
||||
/// </summary>
|
||||
public bool ShowUsernameTooltip
|
||||
{
|
||||
set => clickableArea.TooltipText = value ? (user?.Username ?? string.Empty) : default_tooltip_text;
|
||||
}
|
||||
|
||||
private readonly User user;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private OsuGame game { get; set; }
|
||||
|
||||
private readonly ClickableArea clickableArea;
|
||||
|
||||
/// <summary>
|
||||
/// A clickable avatar for the specified user, with UI sounds included.
|
||||
/// If <see cref="OpenOnClick"/> is <c>true</c>, clicking will open the user's profile.
|
||||
@ -31,35 +46,35 @@ namespace osu.Game.Users.Drawables
|
||||
public ClickableAvatar(User user = null)
|
||||
{
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(LargeTextureStore textures)
|
||||
{
|
||||
ClickableArea clickableArea;
|
||||
Add(clickableArea = new ClickableArea
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Action = openProfile
|
||||
});
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(LargeTextureStore textures)
|
||||
{
|
||||
LoadComponentAsync(new DrawableAvatar(user), clickableArea.Add);
|
||||
|
||||
clickableArea.Enabled.BindTo(OpenOnClick);
|
||||
}
|
||||
|
||||
private void openProfile()
|
||||
{
|
||||
if (!OpenOnClick.Value)
|
||||
return;
|
||||
|
||||
if (user?.Id > 1)
|
||||
game?.ShowUser(user.Id);
|
||||
}
|
||||
|
||||
private class ClickableArea : OsuClickableContainer
|
||||
{
|
||||
public override string TooltipText => Enabled.Value ? @"view profile" : null;
|
||||
private string tooltip = default_tooltip_text;
|
||||
|
||||
public override string TooltipText
|
||||
{
|
||||
get => Enabled.Value ? tooltip : null;
|
||||
set => tooltip = value;
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
|
@ -1,7 +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.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
@ -45,33 +44,38 @@ namespace osu.Game.Users.Drawables
|
||||
|
||||
protected override double LoadDelay => 200;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to show a default guest representation on null user (as opposed to nothing).
|
||||
/// </summary>
|
||||
public bool ShowGuestOnNull = true;
|
||||
private readonly bool openOnClick;
|
||||
private readonly bool showUsernameTooltip;
|
||||
private readonly bool showGuestOnNull;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to open the user's profile when clicked.
|
||||
/// Construct a new UpdateableAvatar.
|
||||
/// </summary>
|
||||
public readonly BindableBool OpenOnClick = new BindableBool(true);
|
||||
|
||||
public UpdateableAvatar(User user = null)
|
||||
/// <param name="user">The initial user to display.</param>
|
||||
/// <param name="openOnClick">Whether to open the user's profile when clicked.</param>
|
||||
/// <param name="showUsernameTooltip">Whether to show the username rather than "view profile" on the tooltip.</param>
|
||||
/// <param name="showGuestOnNull">Whether to show a default guest representation on null user (as opposed to nothing).</param>
|
||||
public UpdateableAvatar(User user = null, bool openOnClick = true, bool showUsernameTooltip = false, bool showGuestOnNull = true)
|
||||
{
|
||||
this.openOnClick = openOnClick;
|
||||
this.showUsernameTooltip = showUsernameTooltip;
|
||||
this.showGuestOnNull = showGuestOnNull;
|
||||
|
||||
User = user;
|
||||
}
|
||||
|
||||
protected override Drawable CreateDrawable(User user)
|
||||
{
|
||||
if (user == null && !ShowGuestOnNull)
|
||||
if (user == null && !showGuestOnNull)
|
||||
return null;
|
||||
|
||||
var avatar = new ClickableAvatar(user)
|
||||
{
|
||||
OpenOnClick = openOnClick,
|
||||
ShowUsernameTooltip = showUsernameTooltip,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
avatar.OpenOnClick.BindTo(OpenOnClick);
|
||||
|
||||
return avatar;
|
||||
}
|
||||
}
|
||||
|
@ -48,11 +48,7 @@ namespace osu.Game.Users
|
||||
statusIcon.FinishTransforms();
|
||||
}
|
||||
|
||||
protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar
|
||||
{
|
||||
User = User,
|
||||
OpenOnClick = { Value = false }
|
||||
};
|
||||
protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar(User, false);
|
||||
|
||||
protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country)
|
||||
{
|
||||
|
@ -60,6 +60,9 @@ namespace osu.Game.Utils
|
||||
{
|
||||
foreach (var invalid in combination.Where(m => type.IsInstanceOfType(m)))
|
||||
{
|
||||
if (invalid == mod)
|
||||
continue;
|
||||
|
||||
invalidMods ??= new List<Mod>();
|
||||
invalidMods.Add(invalid);
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="AutoMapper" Version="10.1.1" />
|
||||
<PackageReference Include="DiffPlex" Version="1.7.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.33" />
|
||||
<PackageReference Include="Humanizer" Version="2.10.1" />
|
||||
@ -34,8 +35,9 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2021.614.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.614.0" />
|
||||
<PackageReference Include="Realm" Version="10.2.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2021.616.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.618.0" />
|
||||
<PackageReference Include="Sentry" Version="3.4.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.28.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
|
Reference in New Issue
Block a user