1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 13:37:25 +08:00

Merge branch 'master' into fix-autoplay-mod-user-id

This commit is contained in:
Bartłomiej Dach 2022-03-30 21:41:45 +02:00 committed by GitHub
commit 9621a7f9cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 326 additions and 110 deletions

View File

@ -27,7 +27,7 @@ namespace osu.Game.Benchmarks
storage = new TemporaryNativeStorage("realm-benchmark");
storage.DeleteDirectory(string.Empty);
realm = new RealmAccess(storage, "client");
realm = new RealmAccess(storage, OsuGameBase.CLIENT_DATABASE_FILENAME);
realm.Run(r =>
{

View File

@ -39,7 +39,7 @@ namespace osu.Game.Tests.Database
// ReSharper disable once AccessToDisposedClosure
var testStorage = new OsuStorage(host, storage.GetStorageForDirectory(caller));
using (var realm = new RealmAccess(testStorage, "client"))
using (var realm = new RealmAccess(testStorage, OsuGameBase.CLIENT_DATABASE_FILENAME))
{
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realm.Filename)}");
testAction(realm, testStorage);
@ -62,7 +62,7 @@ namespace osu.Game.Tests.Database
{
var testStorage = storage.GetStorageForDirectory(caller);
using (var realm = new RealmAccess(testStorage, "client"))
using (var realm = new RealmAccess(testStorage, OsuGameBase.CLIENT_DATABASE_FILENAME))
{
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realm.Filename)}");
await testAction(realm, testStorage);

View File

@ -143,14 +143,14 @@ namespace osu.Game.Tests.NonVisual
Assert.That(osuStorage, Is.Not.Null);
// In the following tests, realm files are ignored as
// - in the case of checking the source, interacting with the pipe files (client.realm.note) may
// - in the case of checking the source, interacting with the pipe files (.realm.note) may
// lead to unexpected behaviour.
// - in the case of checking the destination, the files may have already been recreated by the game
// as part of the standard migration flow.
foreach (string file in osuStorage.IgnoreFiles)
{
if (!file.Contains("realm", StringComparison.Ordinal))
if (!file.Contains(".realm", StringComparison.Ordinal))
{
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
Assert.That(storage.Exists(file), Is.False, () => $"{file} exists in destination when it was expected to be ignored");
@ -159,7 +159,7 @@ namespace osu.Game.Tests.NonVisual
foreach (string dir in osuStorage.IgnoreDirectories)
{
if (!dir.Contains("realm", StringComparison.Ordinal))
if (!dir.Contains(".realm", StringComparison.Ordinal))
{
Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir)));
Assert.That(storage.Exists(dir), Is.False, () => $"{dir} exists in destination when it was expected to be ignored");
@ -188,19 +188,17 @@ namespace osu.Game.Tests.NonVisual
{
var osu = LoadOsuIntoHost(host);
const string database_filename = "client.realm";
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.That(File.Exists(Path.Combine(customPath, database_filename)));
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
Assert.DoesNotThrow(() => osu.Migrate(customPath2));
Assert.That(File.Exists(Path.Combine(customPath2, database_filename)));
Assert.That(File.Exists(Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME)));
// some files may have been left behind for whatever reason, but that's not what we're testing here.
cleanupPath(customPath);
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.That(File.Exists(Path.Combine(customPath, database_filename)));
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
}
finally
{
@ -233,6 +231,46 @@ namespace osu.Game.Tests.NonVisual
}
}
[Test]
public void TestMigrationFailsOnExistingData()
{
string customPath = prepareCustomPath();
string customPath2 = prepareCustomPath();
using (var host = new CustomTestHeadlessGameHost())
{
try
{
var osu = LoadOsuIntoHost(host);
var storage = osu.Dependencies.Get<Storage>();
var osuStorage = storage as OsuStorage;
string originalDirectory = storage.GetFullPath(".");
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
Directory.CreateDirectory(customPath2);
File.Copy(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME), Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME));
// Fails because file already exists.
Assert.Throws<ArgumentException>(() => osu.Migrate(customPath2));
osuStorage?.ChangeDataPath(customPath2);
Assert.That(osuStorage?.CustomStoragePath, Is.EqualTo(customPath2));
Assert.That(new StreamReader(Path.Combine(originalDirectory, "storage.ini")).ReadToEnd().Contains($"FullPath = {customPath2}"));
}
finally
{
host.Exit();
cleanupPath(customPath);
cleanupPath(customPath2);
}
}
}
[Test]
public void TestMigrationToNestedTargetFails()
{

View File

@ -3,7 +3,9 @@
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using NUnit.Framework;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Database;
@ -28,6 +30,23 @@ namespace osu.Game.Tests.Visual.Navigation
stream.CopyTo(outStream);
}
[SetUp]
public void SetUp()
{
if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && RuntimeInformation.OSArchitecture == Architecture.Arm64)
Assert.Ignore("EF-to-realm migrations are not supported on M1 ARM architectures.");
}
public override void SetUpSteps()
{
// base SetUpSteps are executed before the above SetUp, therefore early-return to allow ignoring test properly.
// attempting to ignore here would yield a TargetInvocationException instead.
if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && RuntimeInformation.OSArchitecture == Architecture.Arm64)
return;
base.SetUpSteps();
}
[Test]
public void TestMigration()
{

View File

@ -116,7 +116,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private void testBeatmapLabels(Ruleset ruleset)
{
AddAssert("check version", () => infoWedge.Info.VersionLabel.Current.Value == $"{ruleset.ShortName}Version");
AddAssert("check title", () => infoWedge.Info.TitleLabel.Current.Value == $"{ruleset.ShortName}Source — {ruleset.ShortName}Title");
AddAssert("check title", () => infoWedge.Info.TitleLabel.Current.Value == $"{ruleset.ShortName}Title");
AddAssert("check artist", () => infoWedge.Info.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist");
AddAssert("check author", () => infoWedge.Info.MapperContainer.ChildrenOfType<OsuSpriteText>().Any(s => s.Current.Value == $"{ruleset.ShortName}Author"));
}

View File

@ -68,7 +68,9 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("reset defaults", () =>
{
Ruleset.Value = new OsuRuleset().RulesetInfo;
Beatmap.SetDefault();
SelectedMods.SetDefault();
songSelect = null;
});
@ -563,7 +565,7 @@ namespace osu.Game.Tests.Visual.SongSelect
}
[Test]
public void TestAutoplayViaCtrlEnter()
public void TestAutoplayShortcut()
{
addRulesetImportStep(0);
@ -580,11 +582,65 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader);
AddAssert("autoplay enabled", () => songSelect.Mods.Value.FirstOrDefault() is ModAutoplay);
AddAssert("autoplay selected", () => songSelect.Mods.Value.Single() is ModAutoplay);
AddUntilStep("wait for return to ss", () => songSelect.IsCurrentScreen());
AddAssert("mod disabled", () => songSelect.Mods.Value.Count == 0);
AddAssert("no mods selected", () => songSelect.Mods.Value.Count == 0);
}
[Test]
public void TestAutoplayShortcutKeepsAutoplayIfSelectedAlready()
{
addRulesetImportStep(0);
createSongSelect();
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
changeMods(new OsuModAutoplay());
AddStep("press ctrl+enter", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Enter);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader);
AddAssert("autoplay selected", () => songSelect.Mods.Value.Single() is ModAutoplay);
AddUntilStep("wait for return to ss", () => songSelect.IsCurrentScreen());
AddAssert("autoplay still selected", () => songSelect.Mods.Value.Single() is ModAutoplay);
}
[Test]
public void TestAutoplayShortcutReturnsInitialModsOnExit()
{
addRulesetImportStep(0);
createSongSelect();
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
changeMods(new OsuModRelax());
AddStep("press ctrl+enter", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Enter);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader);
AddAssert("only autoplay selected", () => songSelect.Mods.Value.Single() is ModAutoplay);
AddUntilStep("wait for return to ss", () => songSelect.IsCurrentScreen());
AddAssert("relax returned", () => songSelect.Mods.Value.Single() is ModRelax);
}
[Test]

View File

@ -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.
#nullable enable
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Graphics;
@ -14,6 +18,7 @@ using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Models;
@ -29,8 +34,6 @@ using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Writers.Zip;
#nullable enable
namespace osu.Game.Database
{
internal class EFToRealmMigrator : CompositeDrawable
@ -57,7 +60,7 @@ namespace osu.Game.Database
[Resolved]
private Storage storage { get; set; } = null!;
private readonly OsuSpriteText currentOperationText;
private readonly OsuTextFlowContainer currentOperationText;
public EFToRealmMigrator()
{
@ -99,11 +102,13 @@ namespace osu.Game.Database
{
State = { Value = Visibility.Visible }
},
currentOperationText = new OsuSpriteText
currentOperationText = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 30))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(size: 30)
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
TextAnchor = Anchor.TopCentre,
},
}
},
@ -147,19 +152,34 @@ namespace osu.Game.Database
log("Migration successful!");
if (DebugUtils.IsDebugBuild)
Logger.Log("Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", level: LogLevel.Important);
{
Logger.Log(
"Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.",
level: LogLevel.Important);
}
}
else
{
log("Migration failed!");
Logger.Log(t.Exception.ToString(), LoggingTarget.Database);
if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && t.Exception.Flatten().InnerException is TypeInitializationException)
{
// Not guaranteed to be the only cause of exception, but let's roll with it for now.
log("Please download and run the intel version of osu! once\nto allow data migration to complete!");
efContextFactory.SetMigrationCompletion();
return;
}
notificationOverlay.Post(new SimpleErrorNotification
{
Text = "IMPORTANT: During data migration, some of your data could not be successfully migrated. The previous version has been backed up.\n\nFor further assistance, please open a discussion on github and attach your backup files (click to get started).",
Text =
"IMPORTANT: During data migration, some of your data could not be successfully migrated. The previous version has been backed up.\n\nFor further assistance, please open a discussion on github and attach your backup files (click to get started).",
Activated = () =>
{
game.OpenUrlExternally($@"https://github.com/ppy/osu/discussions/new?title=Realm%20migration%20issue ({t.Exception.Message})&body=Please%20drag%20the%20""attach_me.zip""%20file%20here!&category=q-a", true);
game.OpenUrlExternally(
$@"https://github.com/ppy/osu/discussions/new?title=Realm%20migration%20issue ({t.Exception.Message})&body=Please%20drag%20the%20""attach_me.zip""%20file%20here!&category=q-a",
true);
const string attachment_filename = "attach_me.zip";
const string backup_folder = "backups";

View File

@ -13,6 +13,7 @@ using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Input.Bindings;
@ -211,7 +212,7 @@ namespace osu.Game.Database
if (realm.All<ScoreInfo>().Any())
{
Logger.Log(@"Recovery aborted as the existing database has scores set already.", LoggingTarget.Database);
Logger.Log(@"To perform recovery, delete client.realm while osu! is not running.", LoggingTarget.Database);
Logger.Log($@"To perform recovery, delete {OsuGameBase.CLIENT_DATABASE_FILENAME} while osu! is not running.", LoggingTarget.Database);
return;
}
}
@ -293,7 +294,18 @@ namespace osu.Game.Database
/// Compact this realm.
/// </summary>
/// <returns></returns>
public bool Compact() => Realm.Compact(getConfiguration());
public bool Compact()
{
try
{
return Realm.Compact(getConfiguration());
}
// Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator).
catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException)
{
return true;
}
}
/// <summary>
/// Run work on realm with a return value.
@ -542,6 +554,11 @@ namespace osu.Game.Database
return Realm.GetInstance(getConfiguration());
}
// Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator).
catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException)
{
return Realm.GetInstance();
}
finally
{
if (tookSemaphoreLock)

View File

@ -36,15 +36,15 @@ namespace osu.Game.IO
public override string[] IgnoreDirectories => new[]
{
"cache",
"client.realm.management"
$"{OsuGameBase.CLIENT_DATABASE_FILENAME}.management",
};
public override string[] IgnoreFiles => new[]
{
"framework.ini",
"storage.ini",
"client.realm.note",
"client.realm.lock",
$"{OsuGameBase.CLIENT_DATABASE_FILENAME}.note",
$"{OsuGameBase.CLIENT_DATABASE_FILENAME}.lock",
};
public OsuStorage(GameHost host, Storage defaultStorage)
@ -64,12 +64,22 @@ namespace osu.Game.IO
/// </summary>
public void ResetCustomStoragePath()
{
storageConfig.SetValue(StorageConfig.FullPath, string.Empty);
storageConfig.Save();
ChangeDataPath(string.Empty);
ChangeTargetStorage(defaultStorage);
}
/// <summary>
/// Updates the target data path without immediately switching.
/// This does NOT migrate any data.
/// The game should immediately be restarted after calling this.
/// </summary>
public void ChangeDataPath(string newPath)
{
storageConfig.SetValue(StorageConfig.FullPath, newPath);
storageConfig.Save();
}
/// <summary>
/// Attempts to change to the user's custom storage path.
/// </summary>
@ -117,8 +127,7 @@ namespace osu.Game.IO
{
bool cleanupSucceeded = base.Migrate(newStorage);
storageConfig.SetValue(StorageConfig.FullPath, newStorage.GetFullPath("."));
storageConfig.Save();
ChangeDataPath(newStorage.GetFullPath("."));
return cleanupSucceeded;
}

View File

@ -57,6 +57,11 @@ namespace osu.Game
public const string CLIENT_STREAM_NAME = @"lazer";
/// <summary>
/// The filename of the main client database.
/// </summary>
public const string CLIENT_DATABASE_FILENAME = @"client.realm";
public const int SAMPLE_CONCURRENCY = 6;
/// <summary>
@ -200,7 +205,7 @@ namespace osu.Game
if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME))
dependencies.Cache(EFContextFactory = new DatabaseContextFactory(Storage));
dependencies.Cache(realm = new RealmAccess(Storage, "client", Host.UpdateThread, EFContextFactory));
dependencies.Cache(realm = new RealmAccess(Storage, CLIENT_DATABASE_FILENAME, Host.UpdateThread, EFContextFactory));
dependencies.CacheAs<RulesetStore>(RulesetStore = new RealmRulesetStore(realm, Storage));
dependencies.CacheAs<IRulesetStore>(RulesetStore);

View File

@ -3,11 +3,14 @@
using System;
using System.IO;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.IO;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
@ -16,6 +19,12 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
[Resolved]
private Storage storage { get; set; }
[Resolved]
private OsuGameBase game { get; set; }
[Resolved(canBeNull: true)]
private DialogOverlay dialogOverlay { get; set; }
protected override DirectoryInfo InitialPath => new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent;
public override bool AllowExternalScreenChange => false;
@ -32,8 +41,29 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
try
{
if (target.GetDirectories().Length > 0 || target.GetFiles().Length > 0)
var directoryInfos = target.GetDirectories();
var fileInfos = target.GetFiles();
if (directoryInfos.Length > 0 || fileInfos.Length > 0)
{
// Quick test for whether there's already an osu! install at the target path.
if (fileInfos.Any(f => f.Name == OsuGameBase.CLIENT_DATABASE_FILENAME))
{
dialogOverlay.Push(new ConfirmDialog("The target directory already seems to have an osu! install. Use that data instead?", () =>
{
dialogOverlay.Push(new ConfirmDialog("To complete this operation, osu! will close. Please open it again to use the new data location.", () =>
{
(storage as OsuStorage)?.ChangeDataPath(target.FullName);
game.GracefullyExit();
}, () => { }));
},
() => { }));
return;
}
target = target.CreateSubdirectory("osu-lazer");
}
}
catch (Exception e)
{

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mods
public override bool UserPlayable => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) };
public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) };
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mods
public override string Description => "The whole playfield is on a wheel!";
public override double ScoreMultiplier => 1;
public override string SettingDescription => $"{SpinSpeed.Value} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
public override string SettingDescription => $"{SpinSpeed.Value:N2} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
public void Update(Playfield playfield)
{

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
@ -27,6 +29,8 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.ModCinema;
public override string Description => "Watch the video without visual distractions.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAutoplay)).ToArray();
public void ApplyToHUD(HUDOverlay overlay)
{
overlay.ShowHud.Value = false;

View File

@ -90,12 +90,7 @@ namespace osu.Game.Scoring
/// </remarks>
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
/// <returns>The bindable containing the total score.</returns>
public Bindable<long> GetBindableTotalScore([NotNull] ScoreInfo score)
{
var bindable = new TotalScoreBindable(score, this);
configManager?.BindWith(OsuSetting.ScoreDisplayMode, bindable.ScoringMode);
return bindable;
}
public Bindable<long> GetBindableTotalScore([NotNull] ScoreInfo score) => new TotalScoreBindable(score, this, configManager);
/// <summary>
/// Retrieves a bindable that represents the formatted total score string of a <see cref="ScoreInfo"/>.
@ -118,7 +113,11 @@ namespace osu.Game.Scoring
public void GetTotalScore([NotNull] ScoreInfo score, [NotNull] Action<long> callback, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default)
{
GetTotalScoreAsync(score, mode, cancellationToken)
.ContinueWith(task => scheduler.Add(() => callback(task.GetResultSafely())), TaskContinuationOptions.OnlyOnRanToCompletion);
.ContinueWith(task => scheduler.Add(() =>
{
if (!cancellationToken.IsCancellationRequested)
callback(task.GetResultSafely());
}), TaskContinuationOptions.OnlyOnRanToCompletion);
}
/// <summary>
@ -183,8 +182,7 @@ namespace osu.Game.Scoring
/// </summary>
private class TotalScoreBindable : Bindable<long>
{
public readonly Bindable<ScoringMode> ScoringMode = new Bindable<ScoringMode>();
private readonly Bindable<ScoringMode> scoringMode = new Bindable<ScoringMode>();
private readonly ScoreInfo score;
private readonly ScoreManager scoreManager;
@ -195,12 +193,14 @@ namespace osu.Game.Scoring
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to provide the total score of.</param>
/// <param name="scoreManager">The <see cref="ScoreManager"/>.</param>
public TotalScoreBindable(ScoreInfo score, ScoreManager scoreManager)
/// <param name="configManager">The config.</param>
public TotalScoreBindable(ScoreInfo score, ScoreManager scoreManager, OsuConfigManager configManager)
{
this.score = score;
this.scoreManager = scoreManager;
ScoringMode.BindValueChanged(onScoringModeChanged, true);
configManager?.BindWith(OsuSetting.ScoreDisplayMode, scoringMode);
scoringMode.BindValueChanged(onScoringModeChanged, true);
}
private void onScoringModeChanged(ValueChangedEvent<ScoringMode> mode)

View File

@ -36,17 +36,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}
private MultiplayerCountdown countdown;
private DateTimeOffset countdownReceivedTime;
private DateTimeOffset countdownChangeTime;
private ScheduledDelegate countdownUpdateDelegate;
private void onRoomUpdated() => Scheduler.AddOnce(() =>
{
if (countdown == null && room?.Countdown != null)
countdownReceivedTime = DateTimeOffset.Now;
if (countdown != room?.Countdown)
{
countdown = room?.Countdown;
countdownChangeTime = DateTimeOffset.Now;
}
countdown = room?.Countdown;
if (room?.Countdown != null)
if (countdown != null)
countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 100, true);
else
{
@ -74,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
if (countdown != null)
{
TimeSpan timeElapsed = DateTimeOffset.Now - countdownReceivedTime;
TimeSpan timeElapsed = DateTimeOffset.Now - countdownChangeTime;
TimeSpan countdownRemaining;
if (timeElapsed > countdown.TimeRemaining)

View File

@ -80,6 +80,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
Schedule(() =>
{
// If an error or server side trigger occurred this screen may have already exited by external means.
if (!this.IsCurrentScreen())
return;
loadingLayer.Hide();
if (t.IsFaulted)

View File

@ -21,7 +21,9 @@ namespace osu.Game.Screens.Play.HUD
private uint scheduledPopOutCurrentId;
private const double pop_out_duration = 150;
private const double big_pop_out_duration = 300;
private const double small_pop_out_duration = 100;
private const double fade_out_duration = 100;
@ -65,32 +67,28 @@ namespace osu.Game.Screens.Play.HUD
Margin = new MarginPadding(10);
Scale = new Vector2(1.2f);
Scale = new Vector2(1.28f);
InternalChildren = new[]
{
counterContainer = new Container
{
AutoSizeAxes = Axes.Both,
AlwaysPresent = true,
Children = new[]
{
popOutCount = new LegacySpriteText(LegacyFont.Combo)
{
Alpha = 0,
Margin = new MarginPadding(0.05f),
Blending = BlendingParameters.Additive,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
BypassAutoSizeAxes = Axes.Both,
},
displayedCountSpriteText = new LegacySpriteText(LegacyFont.Combo)
{
// Initial text and AlwaysPresent allow the counter to have a size before it first displays a combo.
// This is useful for display in the skin editor.
Text = formatCount(0),
AlwaysPresent = true,
Alpha = 0,
AlwaysPresent = true,
Anchor = Anchor.BottomLeft,
BypassAutoSizeAxes = Axes.Both,
},
}
}
@ -130,8 +128,25 @@ namespace osu.Game.Screens.Play.HUD
base.LoadComplete();
((IHasText)displayedCountSpriteText).Text = formatCount(Current.Value);
((IHasText)popOutCount).Text = formatCount(Current.Value);
Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true);
updateLayout();
}
private void updateLayout()
{
const float font_height_ratio = 0.625f;
const float vertical_offset = 9;
displayedCountSpriteText.OriginPosition = new Vector2(0, font_height_ratio * displayedCountSpriteText.Height + vertical_offset);
displayedCountSpriteText.Position = new Vector2(0, -(1 - font_height_ratio) * displayedCountSpriteText.Height + vertical_offset);
popOutCount.OriginPosition = new Vector2(3, font_height_ratio * popOutCount.Height + vertical_offset); // In stable, the bigger pop out scales a bit to the left
popOutCount.Position = new Vector2(0, -(1 - font_height_ratio) * popOutCount.Height + vertical_offset);
counterContainer.Size = displayedCountSpriteText.Size;
}
private void updateCount(bool rolling)
@ -164,27 +179,31 @@ namespace osu.Game.Screens.Play.HUD
{
((IHasText)popOutCount).Text = formatCount(newValue);
popOutCount.ScaleTo(1.6f);
popOutCount.FadeTo(0.75f);
popOutCount.MoveTo(Vector2.Zero);
popOutCount.ScaleTo(1.56f)
.ScaleTo(1, big_pop_out_duration);
popOutCount.ScaleTo(1, pop_out_duration);
popOutCount.FadeOut(pop_out_duration);
popOutCount.MoveTo(displayedCountSpriteText.Position, pop_out_duration);
popOutCount.FadeTo(0.6f)
.FadeOut(big_pop_out_duration);
}
private void transformNoPopOut(int newValue)
{
((IHasText)displayedCountSpriteText).Text = formatCount(newValue);
counterContainer.Size = displayedCountSpriteText.Size;
displayedCountSpriteText.ScaleTo(1);
}
private void transformPopOutSmall(int newValue)
{
((IHasText)displayedCountSpriteText).Text = formatCount(newValue);
displayedCountSpriteText.ScaleTo(1.1f);
displayedCountSpriteText.ScaleTo(1, pop_out_duration);
counterContainer.Size = displayedCountSpriteText.Size;
displayedCountSpriteText.ScaleTo(1).Then()
.ScaleTo(1.1f, small_pop_out_duration / 2, Easing.In).Then()
.ScaleTo(1, small_pop_out_duration / 2, Easing.Out);
}
private void scheduledPopOutSmall(uint id)
@ -212,7 +231,7 @@ namespace osu.Game.Screens.Play.HUD
Scheduler.AddDelayed(delegate
{
scheduledPopOutSmall(newTaskId);
}, pop_out_duration);
}, big_pop_out_duration - 140);
}
private void onCountRolling(int currentValue, int newValue)

View File

@ -287,12 +287,14 @@ namespace osu.Game.Screens.Select
{
TitleLabel = new OsuSpriteText
{
Current = { BindTarget = titleBinding },
Font = OsuFont.GetFont(size: 28, italics: true),
RelativeSizeAxes = Axes.X,
Truncate = true,
},
ArtistLabel = new OsuSpriteText
{
Current = { BindTarget = artistBinding },
Font = OsuFont.GetFont(size: 17, italics: true),
RelativeSizeAxes = Axes.X,
Truncate = true,
@ -314,9 +316,6 @@ namespace osu.Game.Screens.Select
}
};
titleBinding.BindValueChanged(_ => setMetadata(metadata.Source));
artistBinding.BindValueChanged(_ => setMetadata(metadata.Source), true);
addInfoLabels();
}
@ -352,12 +351,6 @@ namespace osu.Game.Screens.Select
}, true);
}
private void setMetadata(string source)
{
ArtistLabel.Text = artistBinding.Value;
TitleLabel.Text = string.IsNullOrEmpty(source) ? titleBinding.Value : source + " — " + titleBinding.Value;
}
private void addInfoLabels()
{
if (working.Beatmap?.HitObjects?.Any() != true)

View File

@ -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.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
@ -15,13 +16,13 @@ using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Users;
using osu.Game.Utils;
using osuTK.Input;
namespace osu.Game.Screens.Select
{
public class PlaySongSelect : SongSelect
{
private bool removeAutoModOnResume;
private OsuScreen playerLoader;
[Resolved(CanBeNull = true)]
@ -44,25 +45,6 @@ namespace osu.Game.Screens.Select
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
private ModAutoplay getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod();
public override void OnResuming(IScreen last)
{
base.OnResuming(last);
playerLoader = null;
if (removeAutoModOnResume)
{
var autoType = getAutoplayMod()?.GetType();
if (autoType != null)
Mods.Value = Mods.Value.Where(m => m.GetType() != autoType).ToArray();
removeAutoModOnResume = false;
}
}
protected override bool OnKeyDown(KeyDownEvent e)
{
switch (e.Key)
@ -78,10 +60,16 @@ namespace osu.Game.Screens.Select
return base.OnKeyDown(e);
}
private IReadOnlyList<Mod> modsAtGameplayStart;
private ModAutoplay getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod();
protected override bool OnStart()
{
if (playerLoader != null) return false;
modsAtGameplayStart = Mods.Value;
// Ctrl+Enter should start map with autoplay enabled.
if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true)
{
@ -96,13 +84,12 @@ namespace osu.Game.Screens.Select
return false;
}
var mods = Mods.Value;
var mods = Mods.Value.Append(autoInstance).ToArray();
if (mods.All(m => m.GetType() != autoInstance.GetType()))
{
Mods.Value = mods.Append(autoInstance).ToArray();
removeAutoModOnResume = true;
}
if (!ModUtils.CheckCompatibleSet(mods, out var invalid))
mods = mods.Except(invalid).Append(autoInstance).ToArray();
Mods.Value = mods;
}
SampleConfirm?.Play();
@ -138,5 +125,16 @@ namespace osu.Game.Screens.Select
return new SoloPlayer();
}
}
public override void OnResuming(IScreen last)
{
base.OnResuming(last);
if (playerLoader != null)
{
Mods.Value = modsAtGameplayStart;
playerLoader = null;
}
}
}
}

View File

@ -42,6 +42,9 @@ namespace osu.Game.Skinning.Editor
[Resolved]
private OsuColour colours { get; set; }
[Resolved(canBeNull: true)]
private SkinEditorOverlay skinEditorOverlay { get; set; }
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
@ -107,7 +110,7 @@ namespace osu.Game.Skinning.Editor
new EditorMenuItem("Save", MenuItemType.Standard, Save),
new EditorMenuItem("Revert to default", MenuItemType.Destructive, revert),
new EditorMenuItemSpacer(),
new EditorMenuItem("Exit", MenuItemType.Standard, Hide),
new EditorMenuItem("Exit", MenuItemType.Standard, () => skinEditorOverlay?.Hide()),
},
},
}

View File

@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual
[TearDownSteps]
public void TearDownSteps()
{
if (DebugUtils.IsNUnitRunning)
if (DebugUtils.IsNUnitRunning && Game != null)
{
AddStep("exit game", () => Game.Exit());
AddUntilStep("wait for game exit", () => Game.Parent == null);

View File

@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual
Resources = parent.Get<OsuGameBase>().Resources;
realm = new Lazy<RealmAccess>(() => new RealmAccess(LocalStorage, "client", host.UpdateThread));
realm = new Lazy<RealmAccess>(() => new RealmAccess(LocalStorage, OsuGameBase.CLIENT_DATABASE_FILENAME, host.UpdateThread));
RecycleLocalStorage(false);