diff --git a/README.md b/README.md index a1f478e39a..dc36145337 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,9 @@ Clone the repository including submodules Build and run - Using Visual Studio 2017, Rider or Visual Studio Code (configurations are included) -- From command line using `dotnet run --project osu.Desktop` +- From command line using `dotnet run --project osu.Desktop`. When building for non-development purposes, add `-c Release` to gain higher performance. + +Note: If you run from command line under linux, you will need to prefix the output folder to your `LD_LIBRARY_PATH`. See `.vscode/launch.json` for an example If you run into issues building you may need to restore nuget packages (commonly via `dotnet restore`). Visual Studio Code users must run `Restore` task from debug tab before attempt to build. diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index ab0afb08d7..c7ea29f8c0 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -74,42 +74,42 @@ namespace osu.Game.Rulesets.Catch.Beatmaps private void initialiseHyperDash(List objects) { - // todo: add difficulty adjust. - double halfCatcherWidth = CatcherArea.CATCHER_SIZE * (objects.FirstOrDefault()?.Scale ?? 1) / CatchPlayfield.BASE_WIDTH / 2; + List objectWithDroplets = new List(); + foreach (var currentObject in objects) + { + if (currentObject is Fruit) + objectWithDroplets.Add(currentObject); + if (currentObject is JuiceStream) + foreach (var currentJuiceElement in currentObject.NestedHitObjects) + if (!(currentJuiceElement is TinyDroplet)) + objectWithDroplets.Add((CatchHitObject)currentJuiceElement); + } + + objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); + + double halfCatcherWidth = CatcherArea.GetCatcherSize(Beatmap.BeatmapInfo.BaseDifficulty) / 2; int lastDirection = 0; double lastExcess = halfCatcherWidth; - int objCount = objects.Count; - - for (int i = 0; i < objCount - 1; i++) + for (int i = 0; i < objectWithDroplets.Count - 1; i++) { - CatchHitObject currentObject = objects[i]; - - // not needed? - // if (currentObject is TinyDroplet) continue; - - CatchHitObject nextObject = objects[i + 1]; - - // while (nextObject is TinyDroplet) - // { - // if (++i == objCount - 1) break; - // nextObject = objects[i + 1]; - // } + CatchHitObject currentObject = objectWithDroplets[i]; + CatchHitObject nextObject = objectWithDroplets[i + 1]; int thisDirection = nextObject.X > currentObject.X ? 1 : -1; - double timeToNext = nextObject.StartTime - ((currentObject as IHasEndTime)?.EndTime ?? currentObject.StartTime) - 4; + double timeToNext = nextObject.StartTime - currentObject.StartTime; double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth); - - if (timeToNext * CatcherArea.Catcher.BASE_SPEED < distanceToNext) + float distanceToHyper = (float)(timeToNext * CatcherArea.Catcher.BASE_SPEED - distanceToNext); + if (distanceToHyper < 0) { currentObject.HyperDashTarget = nextObject; lastExcess = halfCatcherWidth; } else { - //currentObject.DistanceToHyperDash = timeToNext - distanceToNext; - lastExcess = MathHelper.Clamp(timeToNext - distanceToNext, 0, halfCatcherWidth); + currentObject.DistanceToHyperDash = distanceToHyper; + lastExcess = MathHelper.Clamp(distanceToHyper, 0, halfCatcherWidth); } lastDirection = thisDirection; diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 621fc100c2..773bf576dd 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -27,7 +27,9 @@ namespace osu.Game.Rulesets.Catch.Objects public int ComboIndex { get; set; } /// - /// The distance for a fruit to to next hyper if it's not a hyper. + /// Difference between the distance to the next object + /// and the distance that would have triggered a hyper dash. + /// A value close to 0 indicates a difficult jump (for difficulty calculation). /// public float DistanceToHyperDash { get; set; } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 9460512a8d..be56ccf8c1 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -107,6 +107,11 @@ namespace osu.Game.Rulesets.Catch.UI public bool AttemptCatch(CatchHitObject obj) => MovableCatcher.AttemptCatch(obj); + public static float GetCatcherSize(BeatmapDifficulty difficulty) + { + return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + } + public class Catcher : Container, IKeyBindingHandler { /// diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index af2a889f03..6a0457efc6 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -111,6 +111,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2; } + protected void BeginHold() + { + holdStartTime = Time.Current; + bodyPiece.Hitting = true; + } + + protected void EndHold() + { + holdStartTime = null; + bodyPiece.Hitting = false; + } + public bool OnPressed(ManiaAction action) { // Make sure the action happened within the body of the hold note @@ -123,8 +135,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // The user has pressed during the body of the hold note, after the head note and its hit windows have passed // and within the limited range of the above if-statement. This state will be managed by the head note if the // user has pressed during the hit windows of the head note. - holdStartTime = Time.Current; - + BeginHold(); return true; } @@ -137,7 +148,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (action != Action.Value) return false; - holdStartTime = null; + EndHold(); // If the key has been released too early, the user should not receive full score for the release if (!Tail.IsHit) @@ -170,7 +181,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // The head note also handles early hits before the body, but we want accurate early hits to count as the body being held // The body doesn't handle these early early hits, so we have to explicitly set the holding state here - holdNote.holdStartTime = Time.Current; + holdNote.BeginHold(); return true; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs index 4ab2da208a..619fe06c73 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs @@ -32,6 +32,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces background = new Box { RelativeSizeAxes = Axes.Both }, foreground = new BufferedContainer { + Blending = BlendingMode.Additive, RelativeSizeAxes = Axes.Both, CacheDrawnFrameBuffer = true, Children = new Drawable[] @@ -73,6 +74,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces } private Color4 accentColour; + public Color4 AccentColour { get { return accentColour; } @@ -86,6 +88,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces } } + public bool Hitting + { + get { return hitting; } + set + { + hitting = value; + updateAccentColour(); + } + } + private Cached subtractionCache = new Cached(); public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) @@ -118,13 +130,26 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces } } + private bool hitting; + private void updateAccentColour() { if (!IsLoaded) return; - foreground.Colour = AccentColour.Opacity(0.9f); - background.Colour = AccentColour.Opacity(0.6f); + foreground.Colour = AccentColour.Opacity(0.5f); + background.Colour = AccentColour.Opacity(0.7f); + + const float animation_length = 50; + + foreground.ClearTransforms(false, nameof(foreground.Colour)); + if (hitting) + { + // wait for the next sync point + double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2); + using (foreground.BeginDelayedSequence(synchronisedOffset)) + foreground.FadeColour(AccentColour.Lighten(0.2f), animation_length).Then().FadeColour(foreground.Colour, animation_length).Loop(); + } subtractionCache.Invalidate(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index e69f340184..10cd246172 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableOsuHitObject : DrawableHitObject { - public override bool IsPresent => base.IsPresent || State.Value == ArmedState.Idle && Time.Current >= HitObject.StartTime - HitObject.TimePreempt; + public override bool IsPresent => base.IsPresent || State.Value == ArmedState.Idle && Clock?.CurrentTime >= HitObject.StartTime - HitObject.TimePreempt; private readonly ShakeContainer shakeContainer; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs index 283d6b91f6..6f0197e711 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs @@ -7,14 +7,15 @@ using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Textures; -using OpenTK; using OpenTK.Graphics.ES30; using OpenTK.Graphics; using osu.Framework.Graphics.Primitives; using osu.Game.Rulesets.Objects.Types; +using OpenTK; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -43,6 +44,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public double? SnakedEnd { get; private set; } private Color4 accentColour = Color4.White; + /// /// Used to colour the path. /// @@ -61,6 +63,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } private Color4 borderColour = Color4.White; + /// /// Used to colour the path border. /// @@ -85,6 +88,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private Vector2 topLeftOffset; private readonly Slider slider; + public SliderBody(Slider s) { slider = s; @@ -139,8 +143,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var texture = new Texture(textureWidth, 1); //initialise background - var raw = new RawTexture(textureWidth, 1); - var bytes = raw.Data; + var raw = new Image(textureWidth, 1); const float aa_portion = 0.02f; const float border_portion = 0.128f; @@ -155,19 +158,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces if (progress <= border_portion) { - bytes[i * 4] = (byte)(BorderColour.R * 255); - bytes[i * 4 + 1] = (byte)(BorderColour.G * 255); - bytes[i * 4 + 2] = (byte)(BorderColour.B * 255); - bytes[i * 4 + 3] = (byte)(Math.Min(progress / aa_portion, 1) * (BorderColour.A * 255)); + raw[i, 0] = new Rgba32(BorderColour.R, BorderColour.G, BorderColour.B, Math.Min(progress / aa_portion, 1) * BorderColour.A); } else { progress -= border_portion; - - bytes[i * 4] = (byte)(AccentColour.R * 255); - bytes[i * 4 + 1] = (byte)(AccentColour.G * 255); - bytes[i * 4 + 2] = (byte)(AccentColour.B * 255); - bytes[i * 4 + 3] = (byte)((opacity_at_edge - (opacity_at_edge - opacity_at_centre) * progress / gradient_portion) * (AccentColour.A * 255)); + raw[i, 0] = new Rgba32(AccentColour.R, AccentColour.G, AccentColour.B, + (opacity_at_edge - (opacity_at_edge - opacity_at_centre) * progress / gradient_portion) * AccentColour.A); } } diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index abcd1ddbda..4a6b12d41a 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -216,7 +216,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Texture.DrawQuad( new Quad(pos.X - Size.X / 2, pos.Y - Size.Y / 2, Size.X, Size.Y), - DrawInfo.Colour, + DrawColourInfo.Colour, null, v => Shared.VertexBuffer.Vertices[end++] = new TexturedTrailVertex { diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index f676927404..21df9a6c68 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -11,6 +11,7 @@ using Microsoft.EntityFrameworkCore; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Textures; using osu.Framework.Logging; using osu.Framework.Platform; @@ -62,6 +63,8 @@ namespace osu.Game.Beatmaps public override string[] HandledExtensions => new[] { ".osz" }; + protected override string ImportFromStablePath => "Songs"; + private readonly RulesetStore rulesets; private readonly BeatmapStore beatmaps; @@ -72,11 +75,6 @@ namespace osu.Game.Beatmaps private readonly List currentDownloads = new List(); - /// - /// Set a storage with access to an osu-stable install for import purposes. - /// - public Func GetStableStorage { private get; set; } - public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, AudioManager audioManager, IIpcHost importHost = null) : base(storage, contextFactory, new BeatmapStore(contextFactory), importHost) { @@ -195,7 +193,7 @@ namespace osu.Game.Beatmaps downloadNotification.CompletionClickAction = () => { - PresentBeatmap?.Invoke(importedBeatmap); + PresentCompletedImport(importedBeatmap.Yield()); return true; }; downloadNotification.State = ProgressNotificationState.Completed; @@ -231,6 +229,12 @@ namespace osu.Game.Beatmaps BeatmapDownloadBegan?.Invoke(request); } + protected override void PresentCompletedImport(IEnumerable imported) + { + base.PresentCompletedImport(imported); + PresentBeatmap?.Invoke(imported.LastOrDefault()); + } + /// /// Get an existing download request if it exists. /// @@ -311,27 +315,6 @@ namespace osu.Game.Beatmaps /// Results from the provided query. public IQueryable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); - /// - /// Denotes whether an osu-stable installation is present to perform automated imports from. - /// - public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null; - - /// - /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. - /// - public Task ImportFromStable() - { - var stable = GetStableStorage?.Invoke(); - - if (stable == null) - { - Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error); - return Task.CompletedTask; - } - - return Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs").Select(f => stable.GetFullPath(f)).ToArray()), TaskCreationOptions.LongRunning); - } - /// /// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content. /// diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 43ae30f780..77ff53b893 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Game.Beatmaps.Formats; -using osu.Game.Graphics.Textures; using osu.Game.Skinning; using osu.Game.Storyboards; @@ -45,6 +44,10 @@ namespace osu.Game.Beatmaps private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath; + private LargeTextureStore textureStore; + + protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes. + protected override Texture GetBackground() { if (Metadata?.BackgroundFile == null) @@ -52,7 +55,7 @@ namespace osu.Game.Beatmaps try { - return new LargeTextureStore(new RawTextureLoaderStore(store)).Get(getPathForFile(Metadata.BackgroundFile)); + return (textureStore ?? (textureStore = new LargeTextureStore(new TextureLoaderStore(store)))).Get(getPathForFile(Metadata.BackgroundFile)); } catch { @@ -73,6 +76,14 @@ namespace osu.Game.Beatmaps } } + public override void TransferTo(WorkingBeatmap other) + { + base.TransferTo(other); + + if (other is BeatmapManagerWorkingBeatmap owb && textureStore != null && BeatmapInfo.BackgroundEquals(other.BeatmapInfo)) + owb.textureStore = textureStore; + } + protected override Waveform GetWaveform() { try diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 085c591fce..e0a22460ef 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -8,10 +8,10 @@ using osu.Game.Rulesets.Mods; using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using osu.Game.Storyboards; using osu.Framework.IO.File; using System.IO; +using System.Threading; using osu.Game.IO.Serialization; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; @@ -38,12 +38,26 @@ namespace osu.Game.Beatmaps Mods.ValueChanged += mods => applyRateAdjustments(); - beatmap = new AsyncLazy(populateBeatmap); - background = new AsyncLazy(populateBackground, b => b == null || !b.IsDisposed); - track = new AsyncLazy(populateTrack); - waveform = new AsyncLazy(populateWaveform); - storyboard = new AsyncLazy(populateStoryboard); - skin = new AsyncLazy(populateSkin); + beatmap = new RecyclableLazy(() => + { + var b = GetBeatmap() ?? new Beatmap(); + // use the database-backed info. + b.BeatmapInfo = BeatmapInfo; + return b; + }); + + track = new RecyclableLazy(() => + { + // we want to ensure that we always have a track, even if it's a fake one. + var t = GetTrack() ?? new VirtualBeatmapTrack(Beatmap); + applyRateAdjustments(t); + return t; + }); + + background = new RecyclableLazy(GetBackground, BackgroundStillValid); + waveform = new RecyclableLazy(GetWaveform); + storyboard = new RecyclableLazy(GetStoryboard); + skin = new RecyclableLazy(GetSkin); } /// @@ -58,28 +72,6 @@ namespace osu.Game.Beatmaps return path; } - protected abstract IBeatmap GetBeatmap(); - protected abstract Texture GetBackground(); - protected abstract Track GetTrack(); - protected virtual Skin GetSkin() => new DefaultSkin(); - protected virtual Waveform GetWaveform() => new Waveform(); - protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo }; - - public bool BeatmapLoaded => beatmap.IsResultAvailable; - public IBeatmap Beatmap => beatmap.Value.Result; - public Task GetBeatmapAsync() => beatmap.Value; - private readonly AsyncLazy beatmap; - - private IBeatmap populateBeatmap() - { - var b = GetBeatmap() ?? new Beatmap(); - - // use the database-backed info. - b.BeatmapInfo = BeatmapInfo; - - return b; - } - /// /// Constructs a playable from using the applicable converters for a specific . /// @@ -136,62 +128,53 @@ namespace osu.Game.Beatmaps public override string ToString() => BeatmapInfo.ToString(); - public bool BackgroundLoaded => background.IsResultAvailable; - public Texture Background => background.Value.Result; - public Task GetBackgroundAsync() => background.Value; - private AsyncLazy background; + public bool BeatmapLoaded => beatmap.IsResultAvailable; + public IBeatmap Beatmap => beatmap.Value; + protected abstract IBeatmap GetBeatmap(); + private readonly RecyclableLazy beatmap; - private Texture populateBackground() => GetBackground(); + public bool BackgroundLoaded => background.IsResultAvailable; + public Texture Background => background.Value; + protected virtual bool BackgroundStillValid(Texture b) => b == null || b.Available; + protected abstract Texture GetBackground(); + private readonly RecyclableLazy background; public bool TrackLoaded => track.IsResultAvailable; - public Track Track => track.Value.Result; - public Task GetTrackAsync() => track.Value; - private AsyncLazy track; - - private Track populateTrack() - { - // we want to ensure that we always have a track, even if it's a fake one. - var t = GetTrack() ?? new VirtualBeatmapTrack(Beatmap); - applyRateAdjustments(t); - return t; - } + public Track Track => track.Value; + protected abstract Track GetTrack(); + private RecyclableLazy track; public bool WaveformLoaded => waveform.IsResultAvailable; - public Waveform Waveform => waveform.Value.Result; - public Task GetWaveformAsync() => waveform.Value; - private readonly AsyncLazy waveform; - - private Waveform populateWaveform() => GetWaveform(); + public Waveform Waveform => waveform.Value; + protected virtual Waveform GetWaveform() => new Waveform(); + private readonly RecyclableLazy waveform; public bool StoryboardLoaded => storyboard.IsResultAvailable; - public Storyboard Storyboard => storyboard.Value.Result; - public Task GetStoryboardAsync() => storyboard.Value; - private readonly AsyncLazy storyboard; - - private Storyboard populateStoryboard() => GetStoryboard(); + public Storyboard Storyboard => storyboard.Value; + protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo }; + private readonly RecyclableLazy storyboard; public bool SkinLoaded => skin.IsResultAvailable; - public Skin Skin => skin.Value.Result; - public Task GetSkinAsync() => skin.Value; - private readonly AsyncLazy skin; + public Skin Skin => skin.Value; + protected virtual Skin GetSkin() => new DefaultSkin(); + private readonly RecyclableLazy skin; - private Skin populateSkin() => GetSkin(); - - public void TransferTo(WorkingBeatmap other) + /// + /// Transfer pieces of a beatmap to a new one, where possible, to save on loading. + /// + /// The new beatmap which is being switched to. + public virtual void TransferTo(WorkingBeatmap other) { if (track.IsResultAvailable && Track != null && BeatmapInfo.AudioEquals(other.BeatmapInfo)) other.track = track; - - if (background.IsResultAvailable && Background != null && BeatmapInfo.BackgroundEquals(other.BeatmapInfo)) - other.background = background; } public virtual void Dispose() { - if (BackgroundLoaded) Background?.Dispose(); - if (WaveformLoaded) Waveform?.Dispose(); - if (StoryboardLoaded) Storyboard?.Dispose(); - if (SkinLoaded) Skin?.Dispose(); + background.Recycle(); + waveform.Recycle(); + storyboard.Recycle(); + skin.Recycle(); } /// @@ -210,15 +193,15 @@ namespace osu.Game.Beatmaps mod.ApplyToClock(t); } - public class AsyncLazy + public class RecyclableLazy { - private Lazy> lazy; + private Lazy lazy; private readonly Func valueFactory; private readonly Func stillValidFunction; - private readonly object initLock = new object(); + private readonly object fetchLock = new object(); - public AsyncLazy(Func valueFactory, Func stillValidFunction = null) + public RecyclableLazy(Func valueFactory, Func stillValidFunction = null) { this.valueFactory = valueFactory; this.stillValidFunction = stillValidFunction; @@ -230,45 +213,28 @@ namespace osu.Game.Beatmaps { if (!IsResultAvailable) return; - (lazy.Value.Result as IDisposable)?.Dispose(); + (lazy.Value as IDisposable)?.Dispose(); recreate(); } - public bool IsResultAvailable + public bool IsResultAvailable => stillValid; + + public T Value { get { - recreateIfInvalid(); - return lazy.Value.IsCompleted; + lock (fetchLock) + { + if (!stillValid) + recreate(); + return lazy.Value; + } } } - public Task Value - { - get - { - recreateIfInvalid(); - return lazy.Value; - } - } + private bool stillValid => lazy.IsValueCreated && (stillValidFunction?.Invoke(lazy.Value) ?? true); - private void recreateIfInvalid() - { - lock (initLock) - { - if (!lazy.IsValueCreated || !lazy.Value.IsCompleted) - // we have not yet been initialised or haven't run the task. - return; - - if (stillValidFunction?.Invoke(lazy.Value.Result) ?? true) - // we are still in a valid state. - return; - - recreate(); - } - } - - private void recreate() => lazy = new Lazy>(() => Task.Run(valueFactory)); + private void recreate() => lazy = new Lazy(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication); } } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 326d042c39..f4f169f27c 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using osu.Framework.IO.File; @@ -58,7 +59,7 @@ namespace osu.Game.Database // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) private ArchiveImportIPCChannel ipc; - private readonly List cachedEvents = new List(); + private readonly List queuedEvents = new List(); /// /// Allows delaying of outwards events until an operation is confirmed (at a database level). @@ -76,20 +77,26 @@ namespace osu.Game.Database /// Whether the flushed events should be performed. private void flushEvents(bool perform) { + Action[] events; + lock (queuedEvents) + { + events = queuedEvents.ToArray(); + queuedEvents.Clear(); + } + if (perform) { - foreach (var a in cachedEvents) + foreach (var a in events) a.Invoke(); } - cachedEvents.Clear(); delayingEvents = false; } private void handleEvent(Action a) { if (delayingEvents) - cachedEvents.Add(a); + lock (queuedEvents) queuedEvents.Add(a); else a.Invoke(); } @@ -129,7 +136,6 @@ namespace osu.Game.Database List imported = new List(); int current = 0; - int errors = 0; foreach (string path in paths) { if (notification.State == ProgressNotificationState.Cancelled) @@ -162,12 +168,29 @@ namespace osu.Game.Database { e = e.InnerException ?? e; Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})"); - errors++; } } - notification.Text = errors > 0 ? $"Import complete with {errors} errors" : "Import successful!"; - notification.State = ProgressNotificationState.Completed; + if (imported.Count == 0) + { + notification.Text = "Import failed!"; + notification.State = ProgressNotificationState.Cancelled; + } + else + { + notification.CompletionText = $"Imported {current} {typeof(TModel).Name.Replace("Info", "").ToLower()}s!"; + notification.CompletionClickAction += () => + { + if (imported.Count > 0) + PresentCompletedImport(imported); + return true; + }; + notification.State = ProgressNotificationState.Completed; + } + } + + protected virtual void PresentCompletedImport(IEnumerable imported) + { } /// @@ -283,7 +306,7 @@ namespace osu.Game.Database var notification = new ProgressNotification { Progress = 0, - CompletionText = "Deleted all beatmaps!", + CompletionText = $"Deleted all {typeof(TModel).Name.Replace("Info", "").ToLower()}s!", State = ProgressNotificationState.Active, }; @@ -385,6 +408,41 @@ namespace osu.Game.Database return fileInfos; } + #region osu-stable import + + /// + /// Set a storage with access to an osu-stable install for import purposes. + /// + public Func GetStableStorage { private get; set; } + + /// + /// Denotes whether an osu-stable installation is present to perform automated imports from. + /// + public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null; + + /// + /// The relative path from osu-stable's data directory to import items from. + /// + protected virtual string ImportFromStablePath => null; + + /// + /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. + /// + public Task ImportFromStableAsync() + { + var stable = GetStableStorage?.Invoke(); + + if (stable == null) + { + Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error); + return Task.CompletedTask; + } + + return Task.Factory.StartNew(() => Import(stable.GetDirectories(ImportFromStablePath).Select(f => stable.GetFullPath(f)).ToArray()), TaskCreationOptions.LongRunning); + } + + #endregion + /// /// Create a barebones model from the provided archive. /// Actual expensive population should be done in ; this should just prepare for duplicate checking. diff --git a/osu.Game/Graphics/Backgrounds/Background.cs b/osu.Game/Graphics/Backgrounds/Background.cs index d5825a8c42..6fc8b0e070 100644 --- a/osu.Game/Graphics/Backgrounds/Background.cs +++ b/osu.Game/Graphics/Backgrounds/Background.cs @@ -5,8 +5,8 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using OpenTK.Graphics; -using osu.Game.Graphics.Textures; namespace osu.Game.Graphics.Backgrounds { diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 01b09c0a40..0f382900ce 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -116,7 +116,7 @@ namespace osu.Game.Graphics.Backgrounds float adjustedAlpha = HideAlphaDiscrepancies ? // Cubically scale alpha to make it drop off more sharply. - (float)Math.Pow(DrawInfo.Colour.AverageColour.Linear.A, 3) : + (float)Math.Pow(DrawColourInfo.Colour.AverageColour.Linear.A, 3) : 1; float elapsedSeconds = (float)Time.Elapsed / 1000; @@ -235,7 +235,7 @@ namespace osu.Game.Graphics.Backgrounds Vector2Extensions.Transform(particle.Position * Size + new Vector2(-offset.X, offset.Y), DrawInfo.Matrix) ); - ColourInfo colourInfo = DrawInfo.Colour; + ColourInfo colourInfo = DrawColourInfo.Colour; colourInfo.ApplyChild(particle.Colour); Texture.DrawTriangle( diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 406fc2ffd2..be794d93a6 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -4,7 +4,6 @@ using System; using Humanizer; using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Game.Graphics.Sprites; @@ -16,7 +15,6 @@ namespace osu.Game.Graphics public DrawableDate(DateTimeOffset date) { - AutoSizeAxes = Axes.Both; Font = "Exo2.0-RegularItalic"; Date = date.ToLocalTime(); diff --git a/osu.Game/Graphics/SpriteIcon.cs b/osu.Game/Graphics/SpriteIcon.cs index b72ba7e02f..1b1df45c77 100644 --- a/osu.Game/Graphics/SpriteIcon.cs +++ b/osu.Game/Graphics/SpriteIcon.cs @@ -95,7 +95,7 @@ namespace osu.Game.Graphics { //adjust shadow alpha based on highest component intensity to avoid muddy display of darker text. //squared result for quadratic fall-off seems to give the best result. - var avgColour = (Color4)DrawInfo.Colour.AverageColour; + var avgColour = (Color4)DrawColourInfo.Colour.AverageColour; spriteShadow.Alpha = (float)Math.Pow(Math.Max(Math.Max(avgColour.R, avgColour.G), avgColour.B), 2); diff --git a/osu.Game/Graphics/Sprites/OsuSpriteText.cs b/osu.Game/Graphics/Sprites/OsuSpriteText.cs index c9389bb9e2..8607d51e12 100644 --- a/osu.Game/Graphics/Sprites/OsuSpriteText.cs +++ b/osu.Game/Graphics/Sprites/OsuSpriteText.cs @@ -3,9 +3,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; -using osu.Framework.MathUtils; -using OpenTK; -using OpenTK.Graphics; using osu.Framework.Graphics.Transforms; namespace osu.Game.Graphics.Sprites @@ -19,27 +16,6 @@ namespace osu.Game.Graphics.Sprites Shadow = true; TextSize = FONT_SIZE; } - - protected override Drawable CreateFallbackCharacterDrawable() - { - var tex = GetTextureForCharacter('?'); - - if (tex != null) - { - float adjust = (RNG.NextSingle() - 0.5f) * 2; - return new Sprite - { - Texture = tex, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Scale = new Vector2(1 + adjust * 0.2f), - Rotation = adjust * 15, - Colour = Color4.White, - }; - } - - return base.CreateFallbackCharacterDrawable(); - } } public static class OsuSpriteTextTransformExtensions diff --git a/osu.Game/Graphics/Textures/LargeTextureStore.cs b/osu.Game/Graphics/Textures/LargeTextureStore.cs deleted file mode 100644 index 4dcbb1220d..0000000000 --- a/osu.Game/Graphics/Textures/LargeTextureStore.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Graphics.Textures; -using osu.Framework.IO.Stores; - -namespace osu.Game.Graphics.Textures -{ - /// - /// A texture store that bypasses atlasing. - /// - public class LargeTextureStore : TextureStore - { - public LargeTextureStore(IResourceStore store = null) : base(store, false) - { - } - } -} diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 12935a5ffe..1dda257c95 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -2,12 +2,10 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Threading; -using System.Threading.Tasks; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Logging; @@ -26,7 +24,7 @@ namespace osu.Game.Online.API private const string client_id = @"5"; private const string client_secret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; - private ConcurrentQueue queue = new ConcurrentQueue(); + private readonly Queue queue = new Queue(); /// /// The username/email provided by the user when initiating a login. @@ -55,7 +53,13 @@ namespace osu.Game.Online.API authentication.TokenString = config.Get(OsuSetting.Token); authentication.Token.ValueChanged += onTokenChanged; - Task.Factory.StartNew(run, cancellationToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + var thread = new Thread(run) + { + Name = "APIAccess", + IsBackground = true + }; + + thread.Start(); } private void onTokenChanged(OAuthToken token) => config.Set(OsuSetting.Token, config.Get(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty); @@ -75,10 +79,7 @@ namespace osu.Game.Online.API public void Unregister(IOnlineComponent component) { - Scheduler.Add(delegate - { - components.Remove(component); - }); + Scheduler.Add(delegate { components.Remove(component); }); } public string AccessToken => authentication.RequestAccessToken(); @@ -103,6 +104,7 @@ namespace osu.Game.Online.API log.Add(@"Queueing a ping request"); Queue(new ListChannelsRequest { Timeout = 5000 }); } + break; case APIState.Offline: case APIState.Connecting: @@ -161,18 +163,21 @@ namespace osu.Game.Online.API continue; } - //process the request queue. - APIRequest req; - while (queue.TryPeek(out req)) + while (true) { - if (handleRequest(req)) + APIRequest req; + + lock (queue) { - //we have succeeded, so let's unqueue. - queue.TryDequeue(out req); + if (queue.Count == 0) break; + req = queue.Dequeue(); } + + // TODO: handle failures better + handleRequest(req); } - Thread.Sleep(1); + Thread.Sleep(50); } } @@ -205,7 +210,8 @@ namespace osu.Game.Online.API } catch (WebException we) { - HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode ?? (we.Status == WebExceptionStatus.UnknownError ? HttpStatusCode.NotAcceptable : HttpStatusCode.RequestTimeout); + HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode + ?? (we.Status == WebExceptionStatus.UnknownError ? HttpStatusCode.NotAcceptable : HttpStatusCode.RequestTimeout); // special cases for un-typed but useful message responses. switch (we.Message) @@ -247,6 +253,7 @@ namespace osu.Game.Online.API } private APIState state; + public APIState State { get { return state; } @@ -271,7 +278,10 @@ namespace osu.Game.Online.API public bool IsLoggedIn => LocalUser.Value.Id > 1; - public void Queue(APIRequest request) => queue.Enqueue(request); + public void Queue(APIRequest request) + { + lock (queue) queue.Enqueue(request); + } public event StateChangeDelegate OnStateChange; @@ -279,16 +289,17 @@ namespace osu.Game.Online.API private void flushQueue(bool failOldRequests = true) { - var oldQueue = queue; - - //flush the queue. - queue = new ConcurrentQueue(); - - if (failOldRequests) + lock (queue) { - APIRequest req; - while (oldQueue.TryDequeue(out req)) - req.Fail(new WebException(@"Disconnected from server")); + var oldQueueRequests = queue.ToArray(); + + queue.Clear(); + + if (failOldRequests) + { + foreach (var req in oldQueueRequests) + req.Fail(new WebException(@"Disconnected from server")); + } } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 7f0576608d..6fcb948298 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -299,11 +299,13 @@ namespace osu.Game // This prevents the cursor from showing until we have a screen with CursorVisible = true MenuCursorContainer.CanShowCursor = currentScreen?.CursorVisible ?? false; - // hook up notifications to components. + // todo: all archive managers should be able to be looped here. SkinManager.PostNotification = n => notifications?.Post(n); - BeatmapManager.PostNotification = n => notifications?.Post(n); + SkinManager.GetStableStorage = GetStorageForStableInstall; + BeatmapManager.PostNotification = n => notifications?.Post(n); BeatmapManager.GetStableStorage = GetStorageForStableInstall; + BeatmapManager.PresentBeatmap = PresentBeatmap; AddRange(new Drawable[] diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index bada2a794d..9a5dac35b9 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -24,7 +24,6 @@ using osu.Framework.Input; using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Database; -using osu.Game.Graphics.Textures; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; @@ -109,11 +108,39 @@ namespace osu.Game dependencies.Cache(contextFactory = new DatabaseContextFactory(Host.Storage)); - dependencies.Cache(new LargeTextureStore(new RawTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures")))); + var largeStore = new LargeTextureStore(new TextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures"))); + largeStore.AddStore(new TextureLoaderStore(new OnlineStore())); + dependencies.Cache(largeStore); dependencies.CacheAs(this); dependencies.Cache(LocalConfig); + //this completely overrides the framework default. will need to change once we make a proper FontStore. + dependencies.Cache(Fonts = new FontStore(new GlyphStore(Resources, @"Fonts/FontAwesome"))); + + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/osuFont")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Medium")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-MediumItalic")); + + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Noto-Basic")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Noto-Hangul")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Noto-CJK-Basic")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Noto-CJK-Compatibility")); + + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Regular")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-RegularItalic")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-SemiBold")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-SemiBoldItalic")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Bold")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-BoldItalic")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Light")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-LightItalic")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Black")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-BlackItalic")); + + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Venera")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Venera-Light")); + runMigrations(); dependencies.Cache(SkinManager = new SkinManager(Host.Storage, contextFactory, Host, Audio)); @@ -137,33 +164,6 @@ namespace osu.Game fileImporters.Add(ScoreStore); fileImporters.Add(SkinManager); - //this completely overrides the framework default. will need to change once we make a proper FontStore. - dependencies.Cache(Fonts = new FontStore { ScaleAdjust = 100 }); - - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/FontAwesome")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/osuFont")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Medium")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-MediumItalic")); - - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Noto-Basic")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Noto-Hangul")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Noto-CJK-Basic")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Noto-CJK-Compatibility")); - - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Regular")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-RegularItalic")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-SemiBold")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-SemiBoldItalic")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Bold")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-BoldItalic")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Light")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-LightItalic")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Black")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-BlackItalic")); - - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Venera")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Venera-Light")); - var defaultBeatmap = new DummyWorkingBeatmap(this); beatmap = new OsuBindableBeatmap(defaultBeatmap, Audio); BeatmapManager.DefaultBeatmap = defaultBeatmap; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index 626de14c98..60811d8b12 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -62,7 +62,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores loading = true; getScoresRequest = new GetScoresRequest(beatmap, beatmap.Ruleset); - getScoresRequest.Success += r => Scores = r.Scores; + getScoresRequest.Success += r => Schedule(() => Scores = r.Scores); api.Queue(getScoresRequest); } } @@ -134,5 +134,10 @@ namespace osu.Game.Overlays.BeatmapSet.Scores this.api = api; updateDisplay(); } + + protected override void Dispose(bool isDisposing) + { + getScoresRequest?.Cancel(); + } } } diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/Direct/DirectPanel.cs index 8a22ff7587..2ee1857ca2 100644 --- a/osu.Game/Overlays/Direct/DirectPanel.cs +++ b/osu.Game/Overlays/Direct/DirectPanel.cs @@ -104,8 +104,6 @@ namespace osu.Game.Overlays.Direct beatmaps.ItemAdded += setAdded; } - public override bool DisposeOnDeathRemoval => true; - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs index a27278e002..629b6d6fa4 100644 --- a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs +++ b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs @@ -118,9 +118,9 @@ namespace osu.Game.Overlays.MedalSplash } [BackgroundDependencyLoader] - private void load(OsuColour colours, TextureStore textures) + private void load(OsuColour colours, TextureStore textures, LargeTextureStore largeTextures) { - medalSprite.Texture = textures.Get(medal.ImageUrl); + medalSprite.Texture = largeTextures.Get(medal.ImageUrl); medalGlow.Texture = textures.Get(@"MedalSplash/medal-glow"); description.Colour = colours.BlueLight; } diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 7a07fb970c..254258d098 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -169,7 +169,7 @@ namespace osu.Game.Overlays.Notifications public Action CompletionTarget { get; set; } /// - /// An action to complete when the completion notification is clicked. + /// An action to complete when the completion notification is clicked. Return true to close. /// public Func CompletionClickAction; diff --git a/osu.Game/Overlays/Profile/Header/BadgeContainer.cs b/osu.Game/Overlays/Profile/Header/BadgeContainer.cs index bfade5e45c..33baaf1fff 100644 --- a/osu.Game/Overlays/Profile/Header/BadgeContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BadgeContainer.cs @@ -176,7 +176,7 @@ namespace osu.Game.Overlays.Profile.Header } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { Child = new Sprite { diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index 0b06acd426..621f752b9c 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -14,8 +14,8 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps public class PaginatedBeatmapContainer : PaginatedContainer { private const float panel_padding = 10f; - private readonly BeatmapSetType type; + private GetUserBeatmapsRequest request; public PaginatedBeatmapContainer(BeatmapSetType type, Bindable user, string header, string missing = "None... yet.") : base(user, header, missing) @@ -31,9 +31,8 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps { base.ShowMore(); - var req = new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++ * ItemsPerPage); - - req.Success += sets => + request = new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++ * ItemsPerPage); + request.Success += sets => Schedule(() => { ShowMoreButton.FadeTo(sets.Count == ItemsPerPage ? 1 : 0); ShowMoreLoading.Hide(); @@ -52,9 +51,15 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps var panel = new DirectGridPanel(s.ToBeatmapSet(Rulesets)); ItemsContainer.Add(panel); } - }; + }); - Api.Queue(req); + Api.Queue(request); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + request?.Cancel(); } } } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs index 42784682be..ad886c363b 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs @@ -12,6 +12,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer { + private GetUserMostPlayedBeatmapsRequest request; + public PaginatedMostPlayedBeatmapContainer(Bindable user) :base(user, "Most Played Beatmaps", "No records. :(") { @@ -24,9 +26,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { base.ShowMore(); - var req = new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++ * ItemsPerPage); - - req.Success += beatmaps => + request = new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++ * ItemsPerPage); + request.Success += beatmaps => Schedule(() => { ShowMoreButton.FadeTo(beatmaps.Count == ItemsPerPage ? 1 : 0); ShowMoreLoading.Hide(); @@ -43,9 +44,16 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { ItemsContainer.Add(new DrawableMostPlayedRow(beatmap.GetBeatmapInfo(Rulesets), beatmap.PlayCount)); } - }; + }); - Api.Queue(req); + Api.Queue(request); + } + + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + request?.Cancel(); } } } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index 6dbb9b9ba3..db93fcbc1b 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Profile.Sections protected readonly Bindable User = new Bindable(); protected APIAccess Api; + protected APIRequest RetrievalRequest; protected RulesetStore Rulesets; public PaginatedContainer(Bindable user, string header, string missing) diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 707de0a10f..ed82c62e5c 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -16,6 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks { private readonly bool includeWeight; private readonly ScoreType type; + private GetUserScoresRequest request; public PaginatedScoreContainer(ScoreType type, Bindable user, string header, string missing, bool includeWeight = false) : base(user, header, missing) @@ -32,9 +33,8 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks { base.ShowMore(); - var req = new GetUserScoresRequest(User.Value.Id, type, VisiblePages++ * ItemsPerPage); - - req.Success += scores => + request = new GetUserScoresRequest(User.Value.Id, type, VisiblePages++ * ItemsPerPage); + request.Success += scores => Schedule(() => { foreach (var s in scores) s.ApplyRuleset(Rulesets.GetRuleset(s.OnlineRulesetID)); @@ -66,9 +66,15 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks ItemsContainer.Add(drawableScore); } - }; + }); - Api.Queue(req); + Api.Queue(request); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + request?.Cancel(); } } } diff --git a/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs b/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs index 0d354c728f..45569271df 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { sprite.Texture = textures.Get(url); } diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs index ee2f2f5973..fd5eda4e44 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -12,6 +12,8 @@ namespace osu.Game.Overlays.Profile.Sections.Recent { public class PaginatedRecentActivityContainer : PaginatedContainer { + private GetUserRecentActivitiesRequest request; + public PaginatedRecentActivityContainer(Bindable user, string header, string missing) : base(user, header, missing) { @@ -22,9 +24,8 @@ namespace osu.Game.Overlays.Profile.Sections.Recent { base.ShowMore(); - var req = new GetUserRecentActivitiesRequest(User.Value.Id, VisiblePages++ * ItemsPerPage); - - req.Success += activities => + request = new GetUserRecentActivitiesRequest(User.Value.Id, VisiblePages++ * ItemsPerPage); + request.Success += activities => Schedule(() => { ShowMoreButton.FadeTo(activities.Count == ItemsPerPage ? 1 : 0); ShowMoreLoading.Hide(); @@ -41,9 +42,15 @@ namespace osu.Game.Overlays.Profile.Sections.Recent { ItemsContainer.Add(new DrawableRecentActivity(activity)); } - }; + }); - Api.Queue(req); + Api.Queue(request); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + request?.Cancel(); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 42028f6bd5..4a8164c6df 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -1,8 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; +using System.Drawing; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,21 +19,36 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private FillFlowContainer letterboxSettings; private Bindable letterboxing; + private Bindable sizeFullscreen; + + private OsuGameBase game; + private SettingsDropdown resolutionDropdown; + private SettingsEnumDropdown windowModeDropdown; private const int transition_duration = 400; [BackgroundDependencyLoader] - private void load(FrameworkConfigManager config) + private void load(FrameworkConfigManager config, OsuGameBase game) { + this.game = game; + letterboxing = config.GetBindable(FrameworkSetting.Letterboxing); + sizeFullscreen = config.GetBindable(FrameworkSetting.SizeFullscreen); Children = new Drawable[] { - new SettingsEnumDropdown + windowModeDropdown = new SettingsEnumDropdown { LabelText = "Screen mode", Bindable = config.GetBindable(FrameworkSetting.WindowMode), }, + resolutionDropdown = new SettingsDropdown + { + LabelText = "Resolution", + ShowsDefaultIndicator = false, + Items = getResolutions(), + Bindable = sizeFullscreen + }, new SettingsCheckbox { LabelText = "Letterboxing", @@ -62,15 +81,39 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics }, }; - letterboxing.ValueChanged += isVisible => + windowModeDropdown.Bindable.BindValueChanged(windowMode => + { + if (windowMode == WindowMode.Fullscreen) + { + resolutionDropdown.Show(); + sizeFullscreen.TriggerChange(); + } + else + resolutionDropdown.Hide(); + }, true); + + letterboxing.BindValueChanged(isVisible => { letterboxSettings.ClearTransforms(); letterboxSettings.AutoSizeAxes = isVisible ? Axes.Y : Axes.None; if (!isVisible) letterboxSettings.ResizeHeightTo(0, transition_duration, Easing.OutQuint); - }; - letterboxing.TriggerChange(); + }, true); + } + + private IEnumerable> getResolutions() + { + var resolutions = new KeyValuePair("Default", new Size(9999, 9999)).Yield(); + + if (game.Window != null) + resolutions = resolutions.Concat(game.Window.AvailableResolutions + .Where(r => r.Width >= 800 && r.Height >= 600) + .OrderByDescending(r => r.Width) + .ThenByDescending(r => r.Height) + .Select(res => new KeyValuePair($"{res.Width}x{res.Height}", new Size(res.Width, res.Height))) + .Distinct()).ToList(); + return resolutions; } } } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 93d1986e3a..57e9a528d2 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; +using osu.Game.Skinning; namespace osu.Game.Overlays.Settings.Sections.Maintenance { @@ -20,7 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance protected override string Header => "General"; [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps, DialogOverlay dialogOverlay) + private void load(BeatmapManager beatmaps, SkinManager skins, DialogOverlay dialogOverlay) { Children = new Drawable[] { @@ -30,7 +31,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Action = () => { importButton.Enabled.Value = false; - beatmaps.ImportFromStable().ContinueWith(t => Schedule(() => importButton.Enabled.Value = true)); + beatmaps.ImportFromStableAsync().ContinueWith(t => Schedule(() => importButton.Enabled.Value = true)); } }, deleteButton = new DangerousSettingsButton @@ -45,6 +46,27 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance })); } }, + importButton = new SettingsButton + { + Text = "Import skins from stable", + Action = () => + { + importButton.Enabled.Value = false; + skins.ImportFromStableAsync().ContinueWith(t => Schedule(() => importButton.Enabled.Value = true)); + } + }, + deleteButton = new DangerousSettingsButton + { + Text = "Delete ALL skins", + Action = () => + { + dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => + { + deleteButton.Enabled.Value = false; + Task.Run(() => skins.Delete(skins.GetAllUserSkins())).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true)); + })); + } + }, restoreButton = new SettingsButton { Text = "Restore all hidden difficulties", diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index df8ebaf4aa..15787d29f7 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -51,10 +51,10 @@ namespace osu.Game.Overlays.Settings.Sections }, }; - skins.ItemAdded += onItemsChanged; - skins.ItemRemoved += onItemsChanged; + skins.ItemAdded += itemAdded; + skins.ItemRemoved += itemRemoved; - reloadSkins(); + skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair(s.ToString(), s.ID)); var skinBindable = config.GetBindable(OsuSetting.Skin); @@ -65,9 +65,8 @@ namespace osu.Game.Overlays.Settings.Sections skinDropdown.Bindable = skinBindable; } - private void reloadSkins() => skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair(s.ToString(), s.ID)); - - private void onItemsChanged(SkinInfo _) => Schedule(reloadSkins); + private void itemRemoved(SkinInfo s) => skinDropdown.Items = skinDropdown.Items.Where(i => i.Value != s.ID); + private void itemAdded(SkinInfo s) => skinDropdown.Items = skinDropdown.Items.Append(new KeyValuePair(s.ToString(), s.ID)); protected override void Dispose(bool isDisposing) { @@ -75,8 +74,8 @@ namespace osu.Game.Overlays.Settings.Sections if (skins != null) { - skins.ItemAdded -= onItemsChanged; - skins.ItemRemoved -= onItemsChanged; + skins.ItemAdded -= itemAdded; + skins.ItemRemoved -= itemRemoved; } } diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs index 9555f0fbc5..58714208c0 100644 --- a/osu.Game/Overlays/Settings/SettingsSection.cs +++ b/osu.Game/Overlays/Settings/SettingsSection.cs @@ -73,7 +73,7 @@ namespace osu.Game.Overlays.Settings }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Children = new[] + Children = new Drawable[] { new OsuSpriteText { diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 2eabf1eb74..cdc3bc2b51 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -83,14 +83,14 @@ namespace osu.Game.Overlays.Toolbar [BackgroundDependencyLoader(true)] private void load(OsuGame osuGame) { - if (osuGame != null) - overlayActivationMode.BindTo(osuGame.OverlayActivationMode); - StateChanged += visibility => { if (overlayActivationMode == OverlayActivation.Disabled) State = Visibility.Hidden; }; + + if (osuGame != null) + overlayActivationMode.BindTo(osuGame.OverlayActivationMode); } public class ToolbarBackground : Container diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 11b68b0e09..ea077ff645 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -73,16 +73,15 @@ namespace osu.Game.Overlays FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.Out); } - public void ShowUser(long userId) - { - if (userId == Header.User.Id) - return; - - ShowUser(new User { Id = userId }); - } + public void ShowUser(long userId) => ShowUser(new User { Id = userId }); public void ShowUser(User user, bool fetchOnline = true) { + Show(); + + if (user.Id == Header?.User.Id) + return; + userReq?.Cancel(); Clear(); lastSection = null; @@ -97,6 +96,7 @@ namespace osu.Game.Overlays new BeatmapsSection(), new KudosuSection() }; + tabs = new ProfileTabControl { RelativeSizeAxes = Axes.X, @@ -161,7 +161,6 @@ namespace osu.Game.Overlays userLoadComplete(user); } - Show(); sectionsContainer.ScrollToTop(); } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 7e3e955740..a274d9b12f 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -7,6 +7,8 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; @@ -165,6 +167,14 @@ namespace osu.Game.Rulesets.Objects.Drawables } } + public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) + { + if (!AllJudged) + return false; + + return base.UpdateSubTreeMasking(source, maskingBounds); + } + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index fb6130fa36..a3cb2f13d0 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -176,7 +176,7 @@ namespace osu.Game.Screens.Menu Vector2 inflation = DrawInfo.MatrixInverse.ExtractScale().Xy; - ColourInfo colourInfo = DrawInfo.Colour; + ColourInfo colourInfo = DrawColourInfo.Colour; colourInfo.ApplyChild(Colour); if (AudioData != null) diff --git a/osu.Game/Screens/Ranking/ResultsPageScore.cs b/osu.Game/Screens/Ranking/ResultsPageScore.cs index 42d8af07b9..040458e0eb 100644 --- a/osu.Game/Screens/Ranking/ResultsPageScore.cs +++ b/osu.Game/Screens/Ranking/ResultsPageScore.cs @@ -29,6 +29,7 @@ namespace osu.Game.Screens.Ranking { public class ResultsPageScore : ResultsPage { + private Container scoreContainer; private ScoreCounter scoreCounter; public ResultsPageScore(Score score, WorkingBeatmap beatmap) : base(score, beatmap) { } @@ -76,7 +77,7 @@ namespace osu.Game.Screens.Ranking Size = new Vector2(150, 60), Margin = new MarginPadding(20), }, - new Container + scoreContainer = new Container { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -92,8 +93,8 @@ namespace osu.Game.Screens.Ranking }, scoreCounter = new SlowScoreCounter(6) { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Colour = colours.PinkDarker, Y = 10, TextSize = 56, @@ -185,6 +186,13 @@ namespace osu.Game.Screens.Ranking }); } + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + scoreCounter.Scale = new Vector2(Math.Min(1f, (scoreContainer.DrawWidth - 20) / scoreCounter.DrawWidth)); + } + private class DrawableScoreStatistic : Container { private readonly KeyValuePair statistic; @@ -368,7 +376,7 @@ namespace osu.Game.Screens.Ranking } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { if (!string.IsNullOrEmpty(user.CoverUrl)) cover.Texture = textures.Get(user.CoverUrl); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 7bb0b95b9a..b6cbaf45e9 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -486,6 +486,15 @@ namespace osu.Game.Screens.Select updateItem(p, halfHeight); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + // aggressively dispose "off-screen" items to reduce GC pressure. + foreach (var i in Items) + i.Dispose(); + } + private CarouselBeatmapSet createCarouselSet(BeatmapSetInfo beatmapSet) { if (beatmapSet.Beatmaps.All(b => b.Hidden)) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index d554a22735..23f338b530 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -50,12 +50,12 @@ namespace osu.Game.Screens.Select.Carousel Children = new Drawable[] { - new DelayedLoadWrapper( + new DelayedLoadUnloadWrapper(() => new PanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) { RelativeSizeAxes = Axes.Both, OnLoadComplete = d => d.FadeInFromZero(1000, Easing.OutQuint), - }, 300 + }, 300, 5000 ), new FillFlowContainer { diff --git a/osu.Game/Screens/Select/ImportFromStablePopup.cs b/osu.Game/Screens/Select/ImportFromStablePopup.cs index 7c3b98cc2e..a52b5bab2a 100644 --- a/osu.Game/Screens/Select/ImportFromStablePopup.cs +++ b/osu.Game/Screens/Select/ImportFromStablePopup.cs @@ -12,7 +12,7 @@ namespace osu.Game.Screens.Select public ImportFromStablePopup(Action importFromStable) { HeaderText = @"You have no beatmaps!"; - BodyText = "An existing copy of osu! was found, though.\nWould you like to import your beatmaps?"; + BodyText = "An existing copy of osu! was found, though.\nWould you like to import your beatmaps (and skins)?"; Icon = FontAwesome.fa_plane; diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index e914eb365e..2c43b333aa 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using OpenTK.Input; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -20,6 +19,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens.Edit; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; +using osu.Game.Skinning; namespace osu.Game.Screens.Select { @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Select private readonly Bindable> selectedMods = new Bindable>(new Mod[] { }); [BackgroundDependencyLoader(true)] - private void load(OsuColour colours, AudioManager audio, BeatmapManager beatmaps, DialogOverlay dialogOverlay, Bindable> selectedMods) + private void load(OsuColour colours, AudioManager audio, BeatmapManager beatmaps, SkinManager skins, DialogOverlay dialogOverlay, Bindable> selectedMods) { if (selectedMods != null) this.selectedMods.BindTo(selectedMods); @@ -78,7 +78,10 @@ namespace osu.Game.Screens.Select // if we have no beatmaps but osu-stable is found, let's prompt the user to import. if (!beatmaps.GetAllUsableBeatmapSets().Any() && beatmaps.StableInstallationAvailable) dialogOverlay.Push(new ImportFromStablePopup(() => - Task.Factory.StartNew(beatmaps.ImportFromStable, TaskCreationOptions.LongRunning))); + { + beatmaps.ImportFromStableAsync(); + skins.ImportFromStableAsync(); + })); }); } } diff --git a/osu.Game/Screens/Tournament/Drawings.cs b/osu.Game/Screens/Tournament/Drawings.cs index 63d29d5cd7..4e2109afd0 100644 --- a/osu.Game/Screens/Tournament/Drawings.cs +++ b/osu.Game/Screens/Tournament/Drawings.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Tournament TextureStore flagStore = new TextureStore(); // Local flag store - flagStore.AddStore(new RawTextureLoaderStore(new NamespacedResourceStore(new StorageBackedResourceStore(storage), "Drawings"))); + flagStore.AddStore(new TextureLoaderStore(new NamespacedResourceStore(new StorageBackedResourceStore(storage), "Drawings"))); // Default texture store flagStore.AddStore(textures); diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 9c881c6abb..ce7edf8683 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -37,7 +37,7 @@ namespace osu.Game.Skinning Configuration = new SkinConfiguration(); Samples = audioManager.GetSampleManager(storage); - Textures = new TextureStore(new RawTextureLoaderStore(storage)); + Textures = new TextureStore(new TextureLoaderStore(storage)); } public override Drawable GetDrawableComponent(string componentName) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 2387924cfa..bd694e443a 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -26,17 +26,25 @@ namespace osu.Game.Skinning public override string[] HandledExtensions => new[] { ".osk" }; + protected override string ImportFromStablePath => "Skins"; + /// - /// Returns a list of all usable s. + /// Returns a list of all usable s. Includes the special default skin plus all skins from . /// /// A list of available . public List GetAllUsableSkins() { - var userSkins = ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); + var userSkins = GetAllUserSkins(); userSkins.Insert(0, SkinInfo.Default); return userSkins; } + /// + /// Returns a list of all usable s that have been loaded by the user. + /// + /// A list of available . + public List GetAllUserSkins() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); + protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name @@ -85,6 +93,13 @@ namespace osu.Game.Skinning { this.audio = audio; + ItemRemoved += removedInfo => + { + // check the removed skin is not the current user choice. if it is, switch back to default. + if (removedInfo.ID == CurrentSkinInfo.Value.ID) + CurrentSkinInfo.Value = SkinInfo.Default; + }; + CurrentSkinInfo.ValueChanged += info => CurrentSkin.Value = GetSkin(info); CurrentSkin.ValueChanged += skin => { @@ -93,16 +108,6 @@ namespace osu.Game.Skinning SourceChanged?.Invoke(); }; - - // migrate older imports which didn't have access to skin.ini - using (ContextFactory.GetForWrite()) - { - foreach (var skinInfo in ModelStore.ConsumableItems.Where(s => s.Name.EndsWith(".osk"))) - { - populate(skinInfo); - Update(skinInfo); - } - } } /// diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index d746bb90c4..37c198f370 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -57,7 +57,7 @@ namespace osu.Game.Storyboards.Drawables [BackgroundDependencyLoader] private void load(FileStore fileStore) { - dependencies.Cache(new TextureStore(new RawTextureLoaderStore(fileStore.Store), false) { ScaleAdjust = 1, }); + dependencies.Cache(new TextureStore(new TextureLoaderStore(fileStore.Store), false, scaleAdjust: 1)); foreach (var layer in Storyboard.Layers) Add(layer.CreateDrawable()); diff --git a/osu.Game/Tests/Visual/TestCasePlayer.cs b/osu.Game/Tests/Visual/TestCasePlayer.cs index 9afb1dd6cd..9e499cb961 100644 --- a/osu.Game/Tests/Visual/TestCasePlayer.cs +++ b/osu.Game/Tests/Visual/TestCasePlayer.cs @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual AddStep(r.Name, () => p = loadPlayerFor(r)); AddUntilStep(() => ContinueCondition(p)); - AddAssert("no leaked beatmaps", () => + AddUntilStep(() => { p = null; @@ -64,9 +64,9 @@ namespace osu.Game.Tests.Visual workingWeakReferences.ForEachAlive(_ => count++); return count == 1; - }); + }, "no leaked beatmaps"); - AddAssert("no leaked players", () => + AddUntilStep(() => { GC.Collect(); GC.WaitForPendingFinalizers(); @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual playerWeakReferences.ForEachAlive(_ => count++); return count == 1; - }); + }, "no leaked players"); } } } diff --git a/osu.Game/Users/Avatar.cs b/osu.Game/Users/Avatar.cs index 9ba9549164..e6e1ba3c53 100644 --- a/osu.Game/Users/Avatar.cs +++ b/osu.Game/Users/Avatar.cs @@ -24,7 +24,7 @@ namespace osu.Game.Users } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { if (textures == null) throw new ArgumentNullException(nameof(textures)); diff --git a/osu.Game/Users/UserCoverBackground.cs b/osu.Game/Users/UserCoverBackground.cs index 58b92b2750..fddbd57f9c 100644 --- a/osu.Game/Users/UserCoverBackground.cs +++ b/osu.Game/Users/UserCoverBackground.cs @@ -18,7 +18,7 @@ namespace osu.Game.Users } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { if (textures == null) throw new ArgumentNullException(nameof(textures)); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 669b775674..83ab5534c6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,7 +18,7 @@ - +