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:
commit
9621a7f9cb
@ -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 =>
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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"));
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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";
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user