Merge branch 'master' into spectator-reset-speed-at-end

This commit is contained in:
Salman Ahmed
2022-06-21 06:37:58 +03:00
committed by GitHub
27 changed files with 327 additions and 145 deletions

View File

@ -177,8 +177,17 @@ namespace osu.Game.Beatmaps
}
Beatmap beatmap;
using (var stream = new LineBufferedReader(reader.GetStream(mapName)))
{
if (stream.PeekLine() == null)
{
Logger.Log($"No content found in first .osu file of beatmap archive ({reader.Name} / {mapName})", LoggingTarget.Database);
return null;
}
beatmap = Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
}
return new BeatmapSetInfo
{

View File

@ -101,7 +101,7 @@ namespace osu.Game.Beatmaps
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
b.BeatmapSet = beatmapSet;
var imported = beatmapImporter.Import(beatmapSet);
var imported = beatmapImporter.ImportModel(beatmapSet);
if (imported == null)
throw new InvalidOperationException("Failed to import new beatmap");
@ -409,11 +409,8 @@ namespace osu.Game.Beatmaps
public Task<Live<BeatmapSetInfo>?> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default) =>
beatmapImporter.Import(task, batchImport, cancellationToken);
public Task<Live<BeatmapSetInfo>?> Import(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default) =>
beatmapImporter.Import(archive, batchImport, cancellationToken);
public Live<BeatmapSetInfo>? Import(BeatmapSetInfo item, ArchiveReader? archive = null, CancellationToken cancellationToken = default) =>
beatmapImporter.Import(item, archive, false, cancellationToken);
beatmapImporter.ImportModel(item, archive, false, cancellationToken);
public IEnumerable<string> HandledExtensions => beatmapImporter.HandledExtensions;
@ -457,9 +454,9 @@ namespace osu.Game.Beatmaps
#region Implementation of IPostImports<out BeatmapSetInfo>
public Action<IEnumerable<Live<BeatmapSetInfo>>>? PostImport
public Action<IEnumerable<Live<BeatmapSetInfo>>>? PresentImport
{
set => beatmapImporter.PostImport = value;
set => beatmapImporter.PresentImport = value;
}
#endregion

View File

@ -74,7 +74,7 @@ namespace osu.Game.Beatmaps.Formats
}
if (line == null)
throw new IOException("Unknown file format (null)");
throw new IOException("Unknown file format (no content)");
var decoder = typedDecoders.Where(d => line.StartsWith(d.Key, StringComparison.InvariantCulture)).Select(d => d.Value).FirstOrDefault();

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;
using System.Collections.Generic;
using System.Threading.Tasks;
using osu.Game.Overlays.Notifications;
@ -11,7 +12,7 @@ namespace osu.Game.Database
/// A class which handles importing of associated models to the game store.
/// </summary>
/// <typeparam name="TModel">The model type.</typeparam>
public interface IModelImporter<TModel> : IPostNotifications, IPostImports<TModel>, ICanAcceptFiles
public interface IModelImporter<TModel> : IPostNotifications, ICanAcceptFiles
where TModel : class, IHasGuidPrimaryKey
{
/// <summary>
@ -26,5 +27,10 @@ namespace osu.Game.Database
/// A user displayable name for the model type associated with this manager.
/// </summary>
string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}";
/// <summary>
/// Fired when the user requests to view the resulting import.
/// </summary>
public Action<IEnumerable<Live<TModel>>>? PresentImport { set; }
}
}

View File

@ -1,17 +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;
namespace osu.Game.Database
{
public interface IPostImports<TModel>
where TModel : class, IHasGuidPrimaryKey
{
/// <summary>
/// Fired when the user requests to view the resulting import.
/// </summary>
public Action<IEnumerable<Live<TModel>>>? PostImport { set; }
}
}

View File

@ -33,7 +33,7 @@ namespace osu.Game.Database
}
/// <summary>
/// Construct a new import task from a stream.
/// Construct a new import task from a stream. The provided stream will be disposed after reading.
/// </summary>
public ImportTask(Stream stream, string filename)
{
@ -62,6 +62,7 @@ namespace osu.Game.Database
{
// This isn't used in any current path. May need to reconsider for performance reasons (ie. if we don't expect the incoming stream to be copied out).
memoryStream = new MemoryStream(stream.ReadAllBytesToArray());
stream.Dispose();
}
if (ZipUtils.IsZipArchive(memoryStream))

View File

@ -56,7 +56,7 @@ namespace osu.Game.Database
/// </summary>
private static readonly ThreadedTaskScheduler import_scheduler_batch = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(RealmArchiveModelImporter<TModel>));
public virtual IEnumerable<string> HandledExtensions => new[] { @".zip" };
public abstract IEnumerable<string> HandledExtensions { get; }
protected readonly RealmFileStore Files;
@ -65,7 +65,7 @@ namespace osu.Game.Database
/// <summary>
/// Fired when the user requests to view the resulting import.
/// </summary>
public Action<IEnumerable<Live<TModel>>>? PostImport { get; set; }
public Action<IEnumerable<Live<TModel>>>? PresentImport { get; set; }
/// <summary>
/// Set an endpoint for notifications to be posted to.
@ -158,12 +158,12 @@ namespace osu.Game.Database
? $"Imported {imported.First().GetDisplayString()}!"
: $"Imported {imported.Count} {HumanisedModelName}s!";
if (imported.Count > 0 && PostImport != null)
if (imported.Count > 0 && PresentImport != null)
{
notification.CompletionText += " Click to view.";
notification.CompletionClickAction = () =>
{
PostImport?.Invoke(imported);
PresentImport?.Invoke(imported);
return true;
};
}
@ -188,7 +188,7 @@ namespace osu.Game.Database
Live<TModel>? import;
using (ArchiveReader reader = task.GetReader())
import = await Import(reader, batchImport, cancellationToken).ConfigureAwait(false);
import = await importFromArchive(reader, batchImport, cancellationToken).ConfigureAwait(false);
// We may or may not want to delete the file depending on where it is stored.
// e.g. reconstructing/repairing database with items from default storage.
@ -208,12 +208,15 @@ namespace osu.Game.Database
}
/// <summary>
/// Silently import an item from an <see cref="ArchiveReader"/>.
/// Create and import a model based off the provided <see cref="ArchiveReader"/>.
/// </summary>
/// <remarks>
/// This method also handled queueing the import task on a relevant import thread pool.
/// </remarks>
/// <param name="archive">The archive to be imported.</param>
/// <param name="batchImport">Whether this import is part of a larger batch.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
public async Task<Live<TModel>?> Import(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default)
private async Task<Live<TModel>?> importFromArchive(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
@ -236,7 +239,7 @@ namespace osu.Game.Database
return null;
}
var scheduledImport = Task.Factory.StartNew(() => Import(model, archive, batchImport, cancellationToken),
var scheduledImport = Task.Factory.StartNew(() => ImportModel(model, archive, batchImport, cancellationToken),
cancellationToken,
TaskCreationOptions.HideScheduler,
batchImport ? import_scheduler_batch : import_scheduler);
@ -251,7 +254,7 @@ namespace osu.Game.Database
/// <param name="archive">An optional archive to use for model population.</param>
/// <param name="batchImport">If <c>true</c>, imports will be skipped before they begin, given an existing model matches on hash and filenames. Should generally only be used for large batch imports, as it may defy user expectations when updating an existing model.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
public virtual Live<TModel>? Import(TModel item, ArchiveReader? archive = null, bool batchImport = false, CancellationToken cancellationToken = default) => Realm.Run(realm =>
public virtual Live<TModel>? ImportModel(TModel item, ArchiveReader? archive = null, bool batchImport = false, CancellationToken cancellationToken = default) => Realm.Run(realm =>
{
cancellationToken.ThrowIfCancellationRequested();
@ -335,6 +338,8 @@ namespace osu.Game.Database
transaction.Commit();
}
PostImport(item, realm);
LogForModel(item, @"Import successfully completed!");
}
catch (Exception e)
@ -470,6 +475,15 @@ namespace osu.Game.Database
{
}
/// <summary>
/// Perform any final actions after the import has been committed to the database.
/// </summary>
/// <param name="model">The model prepared for import.</param>
/// <param name="realm">The current realm context.</param>
protected virtual void PostImport(TModel model, Realm realm)
{
}
/// <summary>
/// Check whether an existing model already exists for a new import item.
/// </summary>

View File

@ -637,6 +637,12 @@ namespace osu.Game
Add(performFromMainMenuTask = new PerformFromMenuRunner(action, validScreens, () => ScreenStack.CurrentScreen));
}
public override void AttemptExit()
{
// Using PerformFromScreen gives the user a chance to interrupt the exit process if needed.
PerformFromScreen(menu => menu.Exit());
}
/// <summary>
/// Wait for the game (and target component) to become loaded and then run an action.
/// </summary>
@ -705,13 +711,13 @@ namespace osu.Game
SkinManager.PostNotification = n => Notifications.Post(n);
BeatmapManager.PostNotification = n => Notifications.Post(n);
BeatmapManager.PostImport = items => PresentBeatmap(items.First().Value);
BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value);
BeatmapDownloader.PostNotification = n => Notifications.Post(n);
ScoreDownloader.PostNotification = n => Notifications.Post(n);
ScoreManager.PostNotification = n => Notifications.Post(n);
ScoreManager.PostImport = items => PresentScore(items.First().Value);
ScoreManager.PresentImport = items => PresentScore(items.First().Value);
// make config aware of how to lookup skins for on-screen display purposes.
// if this becomes a more common thing, tracked settings should be reconsidered to allow local DI.

View File

@ -264,14 +264,16 @@ namespace osu.Game
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
dependencies.Cache(difficultyCache = new BeatmapDifficultyCache());
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, () => difficultyCache, LocalConfig));
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, difficultyCache, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true));
dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API));
dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API));
dependencies.Cache(difficultyCache = new BeatmapDifficultyCache());
// Add after all the above cache operations as it depends on them.
AddInternal(difficultyCache);
dependencies.Cache(userCache = new UserLookupCache());
@ -417,14 +419,15 @@ namespace osu.Game
/// <summary>
/// Use to programatically exit the game as if the user was triggering via alt-f4.
/// Will keep persisting until an exit occurs (exit may be blocked multiple times).
/// By default, will keep persisting until an exit occurs (exit may be blocked multiple times).
/// May be interrupted (see <see cref="OsuGame"/>'s override).
/// </summary>
public void GracefullyExit()
public virtual void AttemptExit()
{
if (!OnExiting())
Exit();
else
Scheduler.AddDelayed(GracefullyExit, 2000);
Scheduler.AddDelayed(AttemptExit, 2000);
}
public bool Migrate(string path)

View File

@ -56,7 +56,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
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();
game.Exit();
}, () => { }));
},
() => { }));

View File

@ -27,16 +27,16 @@ namespace osu.Game.Scoring
public class ScoreManager : ModelManager<ScoreInfo>, IModelImporter<ScoreInfo>
{
private readonly Scheduler scheduler;
private readonly Func<BeatmapDifficultyCache> difficulties;
private readonly BeatmapDifficultyCache difficultyCache;
private readonly OsuConfigManager configManager;
private readonly ScoreImporter scoreImporter;
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm, Scheduler scheduler,
Func<BeatmapDifficultyCache> difficulties = null, OsuConfigManager configManager = null)
BeatmapDifficultyCache difficultyCache = null, OsuConfigManager configManager = null)
: base(storage, realm)
{
this.scheduler = scheduler;
this.difficulties = difficulties;
this.difficultyCache = difficultyCache;
this.configManager = configManager;
scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm)
@ -65,8 +65,6 @@ namespace osu.Game.Scoring
/// <returns>The given <paramref name="scores"/> ordered by decreasing total score.</returns>
public async Task<ScoreInfo[]> OrderByTotalScoreAsync(ScoreInfo[] scores, CancellationToken cancellationToken = default)
{
var difficultyCache = difficulties?.Invoke();
if (difficultyCache != null)
{
// Compute difficulties asynchronously first to prevent blocking via the GetTotalScore() call below.
@ -168,11 +166,11 @@ namespace osu.Game.Scoring
return score.BeatmapInfo.MaxCombo.Value;
#pragma warning restore CS0618
if (difficulties == null)
if (difficultyCache == null)
return null;
// We can compute the max combo locally after the async beatmap difficulty computation.
var difficulty = await difficulties().GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
var difficulty = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
return difficulty?.MaxCombo;
}
@ -265,13 +263,13 @@ namespace osu.Game.Scoring
public Task<IEnumerable<Live<ScoreInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => scoreImporter.Import(notification, tasks);
public Live<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default) =>
scoreImporter.Import(item, archive, batchImport, cancellationToken);
scoreImporter.ImportModel(item, archive, batchImport, cancellationToken);
#region Implementation of IPresentImports<ScoreInfo>
public Action<IEnumerable<Live<ScoreInfo>>> PostImport
public Action<IEnumerable<Live<ScoreInfo>>> PresentImport
{
set => scoreImporter.PostImport = value;
set => scoreImporter.PresentImport = value;
}
#endregion

View File

@ -65,6 +65,8 @@ namespace osu.Game.Screens.Edit
base.LoadComplete();
// will be restored via lease, see `DisallowExternalBeatmapRulesetChanges`.
if (!(Beatmap.Value is DummyWorkingBeatmap))
Ruleset.Value = Beatmap.Value.BeatmapInfo.Ruleset;
Mods.Value = Array.Empty<Mod>();
}

View File

@ -19,7 +19,6 @@ using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
@ -141,7 +140,7 @@ namespace osu.Game.Screens.Menu
{
// if we detect that the theme track or beatmap is unavailable this is either first startup or things are in a bad state.
// this could happen if a user has nuked their files store. for now, reimport to repair this.
var import = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream($"Tracks/{BeatmapFile}"), BeatmapFile)).GetResultSafely();
var import = beatmaps.Import(new ImportTask(game.Resources.GetStream($"Tracks/{BeatmapFile}"), BeatmapFile)).GetResultSafely();
import?.PerformWrite(b => b.Protected = true);

View File

@ -786,7 +786,17 @@ namespace osu.Game.Screens.Select
Ruleset.ValueChanged += r => updateSelectedRuleset(r.NewValue);
decoupledRuleset.ValueChanged += r => Ruleset.Value = r.NewValue;
decoupledRuleset.ValueChanged += r =>
{
bool wasDisabled = Ruleset.Disabled;
// a sub-screen may have taken a lease on this decoupled ruleset bindable,
// which would indirectly propagate to the game-global bindable via the `DisabledChanged` callback below.
// to make sure changes sync without crashes, lift the disable for a short while to sync, and then restore the old value.
Ruleset.Disabled = false;
Ruleset.Value = r.NewValue;
Ruleset.Disabled = wasDisabled;
};
decoupledRuleset.DisabledChanged += r => Ruleset.Disabled = r;
Beatmap.BindValueChanged(workingBeatmapChanged);

View File

@ -88,7 +88,7 @@ namespace osu.Game.Skinning.Editor
}
}
private class ToolboxComponentButton : OsuButton
public class ToolboxComponentButton : OsuButton
{
protected override bool ShouldBeConsideredForInput(Drawable child) => false;

View File

@ -322,6 +322,12 @@ namespace osu.Game.Skinning.Editor
protected override bool OnMouseDown(MouseDownEvent e) => true;
public override void Hide()
{
base.Hide();
SelectedComponents.Clear();
}
protected override void PopIn()
{
this

View File

@ -24,7 +24,6 @@ using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Overlays.Notifications;
using osu.Game.Utils;
@ -168,7 +167,7 @@ namespace osu.Game.Skinning
Name = NamingUtils.GetNextBestName(existingSkinNames, $@"{s.Name} (modified)")
};
var result = skinImporter.Import(skinInfo);
var result = skinImporter.ImportModel(skinInfo);
if (result != null)
{
@ -260,9 +259,9 @@ namespace osu.Game.Skinning
#region Implementation of IModelImporter<SkinInfo>
public Action<IEnumerable<Live<SkinInfo>>> PostImport
public Action<IEnumerable<Live<SkinInfo>>> PresentImport
{
set => skinImporter.PostImport = value;
set => skinImporter.PresentImport = value;
}
public Task Import(params string[] paths) => skinImporter.Import(paths);
@ -275,9 +274,6 @@ namespace osu.Game.Skinning
public Task<Live<SkinInfo>> Import(ImportTask task, bool batchImport = false, CancellationToken cancellationToken = default) => skinImporter.Import(task, batchImport, cancellationToken);
public Task<Live<SkinInfo>> Import(ArchiveReader archive, bool batchImport = false, CancellationToken cancellationToken = default) =>
skinImporter.Import(archive, batchImport, cancellationToken);
#endregion
public void Delete([CanBeNull] Expression<Func<SkinInfo, bool>> filter = null, bool silent = false)