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/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 4327abb96f..9460512a8d 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -407,9 +407,7 @@ namespace osu.Game.Rulesets.Catch.UI /// public void Explode() { - var fruit = caughtFruit.ToArray(); - - foreach (var f in fruit) + foreach (var f in caughtFruit.ToArray()) Explode(f); } @@ -422,15 +420,15 @@ namespace osu.Game.Rulesets.Catch.UI fruit.Anchor = Anchor.TopLeft; fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget); - caughtFruit.Remove(fruit); + if (!caughtFruit.Remove(fruit)) + // we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling). + // this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice. + return; ExplodingFruitTarget.Add(fruit); } - fruit.MoveToY(fruit.Y - 50, 250, Easing.OutSine) - .Then() - .MoveToY(fruit.Y + 50, 500, Easing.InSine); - + fruit.MoveToY(fruit.Y - 50, 250, Easing.OutSine).Then().MoveToY(fruit.Y + 50, 500, Easing.InSine); fruit.MoveToX(fruit.X + originalX * 6, 1000); fruit.FadeOut(750); 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/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 46c48bcd0f..3ca92d9dba 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -100,6 +100,11 @@ namespace osu.Game.Beatmaps b.BeatmapSet = beatmapSet; } + validateOnlineIds(beatmapSet.Beatmaps); + + foreach (BeatmapInfo b in beatmapSet.Beatmaps) + fetchAndPopulateOnlineIDs(b, beatmapSet.Beatmaps); + // check if a set already exists with the same online id, delete if it does. if (beatmapSet.OnlineBeatmapSetID != null) { @@ -111,11 +116,6 @@ namespace osu.Game.Beatmaps Logger.Log($"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been purged.", LoggingTarget.Database); } } - - validateOnlineIds(beatmapSet.Beatmaps); - - foreach (BeatmapInfo b in beatmapSet.Beatmaps) - fetchAndPopulateOnlineIDs(b, beatmapSet.Beatmaps); } private void validateOnlineIds(List beatmaps) diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 43ae30f780..cc70aa783f 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 RawTextureLoaderStore(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..a2b44aab52 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.IsDisposed; + 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/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/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index bada2a794d..0037ecf335 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; @@ -114,33 +113,9 @@ namespace osu.Game dependencies.CacheAs(this); dependencies.Cache(LocalConfig); - runMigrations(); - - dependencies.Cache(SkinManager = new SkinManager(Host.Storage, contextFactory, Host, Audio)); - dependencies.CacheAs(SkinManager); - - var api = new APIAccess(LocalConfig); - - dependencies.Cache(api); - dependencies.CacheAs(api); - - dependencies.Cache(RulesetStore = new RulesetStore(contextFactory)); - dependencies.Cache(FileStore = new FileStore(contextFactory, Host.Storage)); - dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory, RulesetStore, api, Audio, Host)); - dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, contextFactory, Host, BeatmapManager, RulesetStore)); - dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); - dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); - dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); - dependencies.Cache(new OsuColour()); - - fileImporters.Add(BeatmapManager); - 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 }); + dependencies.Cache(Fonts = new FontStore(new GlyphStore(Resources, @"Fonts/FontAwesome"))); - 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")); @@ -164,6 +139,29 @@ namespace osu.Game 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)); + dependencies.CacheAs(SkinManager); + + var api = new APIAccess(LocalConfig); + + dependencies.Cache(api); + dependencies.CacheAs(api); + + dependencies.Cache(RulesetStore = new RulesetStore(contextFactory)); + dependencies.Cache(FileStore = new FileStore(contextFactory, Host.Storage)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory, RulesetStore, api, Audio, Host)); + dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, contextFactory, Host, BeatmapManager, RulesetStore)); + dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); + dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); + dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); + dependencies.Cache(new OsuColour()); + + fileImporters.Add(BeatmapManager); + fileImporters.Add(ScoreStore); + fileImporters.Add(SkinManager); + 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 322db0b7d6..8a22ff7587 100644 --- a/osu.Game/Overlays/Direct/DirectPanel.cs +++ b/osu.Game/Overlays/Direct/DirectPanel.cs @@ -187,7 +187,7 @@ namespace osu.Game.Overlays.Direct base.LoadComplete(); this.FadeInFromZero(200, Easing.Out); - PreviewPlaying.ValueChanged += newValue => PlayButton.FadeTo(newValue || IsHovered ? 1 : 0, 120, Easing.InOutQuint); + PreviewPlaying.ValueChanged += newValue => PlayButton.FadeTo(newValue || IsHovered || !FadePlayButton ? 1 : 0, 120, Easing.InOutQuint); PreviewPlaying.ValueChanged += newValue => PreviewBar.FadeTo(newValue ? 1 : 0, 120, Easing.InOutQuint); } 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/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..548d49bd36 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,34 @@ 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() => + 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() ?? new KeyValuePair("Default", new Size(9999, 9999)).Yield(); } } 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/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/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/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index d746bb90c4..d0d4c678bf 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -22,6 +22,7 @@ namespace osu.Game.Storyboards.Drawables public override bool HandleMouseInput => false; private bool passing = true; + public bool Passing { get { return passing; } @@ -36,6 +37,7 @@ namespace osu.Game.Storyboards.Drawables public override bool RemoveCompletedTransforms => false; private DependencyContainer dependencies; + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); @@ -57,7 +59,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 RawTextureLoaderStore(fileStore.Store), false, scaleAdjust: 1)); foreach (var layer in Storyboard.Layers) Add(layer.CreateDrawable()); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 669b775674..cb553731c3 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,7 +18,7 @@ - +