1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 04:52:57 +08:00

Merge branch 'master' into android

This commit is contained in:
Dean Herbert 2019-07-03 19:05:58 +09:00 committed by GitHub
commit 3b5ffdc2cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1035 additions and 294 deletions

View File

@ -1,3 +1,7 @@
<p align="center">
<img width="256" height="256" src="assets/lazer.png">
</p>
# osu! # osu!
[![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu) [![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy) [![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu) [![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
@ -22,8 +26,6 @@ Detailed changelogs are published on the [official osu! site](https://osu.ppy.sh
### Releases ### Releases
![](https://puu.sh/DCmvA/f6a74f5fbb.png)
If you are not interested in developing the game, you can still consume our [binary releases](https://github.com/ppy/osu/releases). If you are not interested in developing the game, you can still consume our [binary releases](https://github.com/ppy/osu/releases).
**Latest build:** **Latest build:**
@ -92,11 +94,13 @@ Code analysis can be run with `powershell ./build.ps1` or `build.sh`. This is cu
We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention on having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time, to ensure no effort is wasted. We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention on having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time, to ensure no effort is wasted.
Please make sure you are familiar with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up. New component development, and where possible, bug fixing and debugging existing components **should always be done under VisualTests**. If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues) (especially those with the ["good first issue"](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) label).
Contributions can be made via pull requests to this repository. We hope to credit and reward larger contributions via a [bounty system](https://www.bountysource.com/teams/ppy). If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues). Before starting, please make sure you are familiar with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up. New component development, and where possible, bug fixing and debugging existing components **should always be done under VisualTests**.
Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured; with any libraries we are using; with any processes involved with contributing, *please* bring it up. I welcome all feedback so we can make contributing to this project as pain-free as possible. Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured; with any libraries we are using; with any processes involved with contributing, *please* bring it up. We welcome all feedback so we can make contributing to this project as pain-free as possible.
For those interested, we love to reward quality contributions via bounties, paid out via paypal or osu! supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project.
## Licence ## Licence

BIN
assets/lazer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -0,0 +1,160 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawable;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneDrawableHitObjects : OsuTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(CatcherArea.Catcher),
typeof(DrawableCatchRuleset),
typeof(DrawableFruit),
typeof(DrawableJuiceStream),
typeof(DrawableBanana)
};
private DrawableCatchRuleset drawableRuleset;
private double playfieldTime => drawableRuleset.Playfield.Time.Current;
[BackgroundDependencyLoader]
private void load()
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.TimingPoints.Add(new TimingControlPoint());
WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
{
HitObjects = new List<HitObject> { new Fruit() },
BeatmapInfo = new BeatmapInfo
{
BaseDifficulty = new BeatmapDifficulty(),
Metadata = new BeatmapMetadata
{
Artist = @"Unknown",
Title = @"You're breathtaking",
AuthorString = @"Everyone",
},
Ruleset = new CatchRuleset().RulesetInfo
},
ControlPointInfo = controlPointInfo
});
Add(new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Children = new[]
{
drawableRuleset = new DrawableCatchRuleset(new CatchRuleset(), beatmap, Array.Empty<Mod>())
}
});
AddStep("miss fruits", () => spawnFruits());
AddStep("hit fruits", () => spawnFruits(true));
AddStep("miss juicestream", () => spawnJuiceStream());
AddStep("hit juicestream", () => spawnJuiceStream(true));
AddStep("miss bananas", () => spawnBananas());
AddStep("hit bananas", () => spawnBananas(true));
}
private void spawnFruits(bool hit = false)
{
for (int i = 1; i <= 4; i++)
{
var fruit = new Fruit
{
X = getXCoords(hit),
LastInCombo = i % 4 == 0,
StartTime = playfieldTime + 800 + (200 * i)
};
fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
addToPlayfield(new DrawableFruit(fruit));
}
}
private void spawnJuiceStream(bool hit = false)
{
var xCoords = getXCoords(hit);
var juice = new JuiceStream
{
X = xCoords,
StartTime = playfieldTime + 1000,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(0, 200)
})
};
juice.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
if (juice.NestedHitObjects.Last() is CatchHitObject tail)
tail.LastInCombo = true; // usually the (Catch)BeatmapProcessor would do this for us when necessary
addToPlayfield(new DrawableJuiceStream(juice, drawableRuleset.CreateDrawableRepresentation));
}
private void spawnBananas(bool hit = false)
{
for (int i = 1; i <= 4; i++)
{
var banana = new Banana
{
X = getXCoords(hit),
LastInCombo = i % 4 == 0,
StartTime = playfieldTime + 800 + (200 * i)
};
banana.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
addToPlayfield(new DrawableBanana(banana));
}
}
private float getXCoords(bool hit)
{
const float x_offset = 0.2f;
float xCoords = drawableRuleset.Playfield.Width / 2;
if (drawableRuleset.Playfield is CatchPlayfield catchPlayfield)
catchPlayfield.CatcherArea.MovableCatcher.X = xCoords - x_offset;
if (hit)
xCoords -= x_offset;
else
xCoords += x_offset;
return xCoords;
}
private void addToPlayfield(DrawableCatchHitObject drawable)
{
foreach (var mod in Mods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawable });
drawableRuleset.Playfield.Add(drawable);
}
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Catch.Mods;
namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneDrawableHitObjectsHidden : TestSceneDrawableHitObjects
{
public override IReadOnlyList<Type> RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(CatchModHidden) }).ToList();
public TestSceneDrawableHitObjectsHidden()
{
Mods.Value = new[] { new CatchModHidden() };
}
}
}

View File

@ -1,7 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawable;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.Mods namespace osu.Game.Rulesets.Catch.Mods
{ {
@ -9,5 +13,36 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public override string Description => @"Play with fading fruits."; public override string Description => @"Play with fading fruits.";
public override double ScoreMultiplier => 1.06; public override double ScoreMultiplier => 1.06;
private const double fade_out_offset_multiplier = 0.6;
private const double fade_out_duration_multiplier = 0.44;
protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state)
{
if (!(drawable is DrawableCatchHitObject catchDrawable))
return;
if (catchDrawable.NestedHitObjects.Any())
{
foreach (var nestedDrawable in catchDrawable.NestedHitObjects)
{
if (nestedDrawable is DrawableCatchHitObject nestedCatchDrawable)
fadeOutHitObject(nestedCatchDrawable);
}
}
else
fadeOutHitObject(catchDrawable);
}
private void fadeOutHitObject(DrawableCatchHitObject drawable)
{
var hitObject = drawable.HitObject;
var offset = hitObject.TimePreempt * fade_out_offset_multiplier;
var duration = offset - hitObject.TimePreempt * fade_out_duration_multiplier;
using (drawable.BeginAbsoluteSequence(hitObject.StartTime - offset, true))
drawable.FadeOut(duration);
}
} }
} }

View File

@ -14,6 +14,8 @@ namespace osu.Game.Rulesets.Catch.Objects
public float X { get; set; } public float X { get; set; }
public double TimePreempt = 1000;
public int IndexInBeatmap { get; set; } public int IndexInBeatmap { get; set; }
public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(ComboIndex % 4); public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(ComboIndex % 4);
@ -54,6 +56,8 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
Scale = 1.0f - 0.7f * (difficulty.CircleSize - 5) / 5; Scale = 1.0f - 0.7f * (difficulty.CircleSize - 5) / 5;
} }

View File

@ -68,11 +68,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
AccentColour = skin.GetValue<SkinConfiguration, Color4?>(s => s.ComboColours.Count > 0 ? s.ComboColours[combo.ComboIndex % s.ComboColours.Count] : (Color4?)null) ?? Color4.White; AccentColour = skin.GetValue<SkinConfiguration, Color4?>(s => s.ComboColours.Count > 0 ? s.ComboColours[combo.ComboIndex % s.ComboColours.Count] : (Color4?)null) ?? Color4.White;
} }
private const float preempt = 1000;
protected override void UpdateState(ArmedState state) protected override void UpdateState(ArmedState state)
{ {
using (BeginAbsoluteSequence(HitObject.StartTime - preempt)) using (BeginAbsoluteSequence(HitObject.StartTime - HitObject.TimePreempt))
this.FadeIn(200); this.FadeIn(200);
var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime;

View File

@ -0,0 +1,76 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Users;
using osuTK;
using System;
using System.Collections.Generic;
namespace osu.Game.Tests.Visual.Gameplay
{
[TestFixture]
public class TestSceneReplayDownloadButton : OsuTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(ReplayDownloadButton)
};
private TestReplayDownloadButton downloadButton;
public TestSceneReplayDownloadButton()
{
createButton(true);
AddStep(@"downloading state", () => downloadButton.SetDownloadState(DownloadState.Downloading));
AddStep(@"locally available state", () => downloadButton.SetDownloadState(DownloadState.LocallyAvailable));
AddStep(@"not downloaded state", () => downloadButton.SetDownloadState(DownloadState.NotDownloaded));
createButton(false);
}
private void createButton(bool withReplay)
{
AddStep(withReplay ? @"create button with replay" : "create button without replay", () =>
{
Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(withReplay))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(80, 40),
};
});
}
private ScoreInfo getScoreInfo(bool replayAvailable)
{
return new APILegacyScoreInfo
{
ID = 1,
OnlineScoreID = 2553163309,
Ruleset = new OsuRuleset().RulesetInfo,
Replay = replayAvailable,
User = new User
{
Id = 39828,
Username = @"WubWoofWolf",
}
};
}
private class TestReplayDownloadButton : ReplayDownloadButton
{
public void SetDownloadState(DownloadState state) => State.Value = state;
public TestReplayDownloadButton(ScoreInfo score)
: base(score)
{
}
}
}
}

View File

@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Online
typeof(BasicStats), typeof(BasicStats),
typeof(BeatmapPicker), typeof(BeatmapPicker),
typeof(Details), typeof(Details),
typeof(DownloadButton), typeof(HeaderDownloadButton),
typeof(FavouriteButton), typeof(FavouriteButton),
typeof(Header), typeof(Header),
typeof(HeaderButton), typeof(HeaderButton),

View File

@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
public override IReadOnlyList<Type> RequiredTypes => new[] public override IReadOnlyList<Type> RequiredTypes => new[]
{ {
typeof(DownloadButton) typeof(PanelDownloadButton)
}; };
private TestDownloadButton downloadButton; private TestDownloadButton downloadButton;
@ -143,7 +143,7 @@ namespace osu.Game.Tests.Visual.Online
return beatmap; return beatmap;
} }
private class TestDownloadButton : DownloadButton private class TestDownloadButton : PanelDownloadButton
{ {
public new bool DownloadEnabled => base.DownloadEnabled; public new bool DownloadEnabled => base.DownloadEnabled;

View File

@ -30,9 +30,9 @@ namespace osu.Game.Tests
trackStore = audioManager.GetTrackStore(reader); trackStore = audioManager.GetTrackStore(reader);
} }
public override void Dispose() protected override void Dispose(bool isDisposing)
{ {
base.Dispose(); base.Dispose(isDisposing);
stream?.Dispose(); stream?.Dispose();
reader?.Dispose(); reader?.Dispose();
trackStore?.Dispose(); trackStore?.Dispose();

View File

@ -13,6 +13,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Lists;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Threading; using osu.Framework.Threading;
@ -159,6 +160,8 @@ namespace osu.Game.Beatmaps
/// <param name="beatmap">The beatmap difficulty to restore.</param> /// <param name="beatmap">The beatmap difficulty to restore.</param>
public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap); public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap);
private readonly WeakList<WorkingBeatmap> workingCache = new WeakList<WorkingBeatmap>();
/// <summary> /// <summary>
/// Retrieve a <see cref="WorkingBeatmap"/> instance for the provided <see cref="BeatmapInfo"/> /// Retrieve a <see cref="WorkingBeatmap"/> instance for the provided <see cref="BeatmapInfo"/>
/// </summary> /// </summary>
@ -173,12 +176,18 @@ namespace osu.Game.Beatmaps
if (beatmapInfo?.BeatmapSet == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo) if (beatmapInfo?.BeatmapSet == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo)
return DefaultBeatmap; return DefaultBeatmap;
var cached = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID);
if (cached != null)
return cached;
if (beatmapInfo.Metadata == null) if (beatmapInfo.Metadata == null)
beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata; beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata;
WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(Files.Store, new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)), beatmapInfo, audioManager); WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(Files.Store, new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)), beatmapInfo, audioManager);
previous?.TransferTo(working); previous?.TransferTo(working);
workingCache.Add(working);
return working; return working;
} }

View File

@ -11,7 +11,9 @@ using osu.Framework.IO.File;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Statistics;
using osu.Game.IO.Serialization; using osu.Game.IO.Serialization;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -31,6 +33,8 @@ namespace osu.Game.Beatmaps
protected AudioManager AudioManager { get; } protected AudioManager AudioManager { get; }
private static readonly GlobalStatistic<int> total_count = GlobalStatistics.Get<int>(nameof(Beatmaps), $"Total {nameof(WorkingBeatmap)}s");
protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager) protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager)
{ {
AudioManager = audioManager; AudioManager = audioManager;
@ -38,24 +42,13 @@ namespace osu.Game.Beatmaps
BeatmapSetInfo = beatmapInfo.BeatmapSet; BeatmapSetInfo = beatmapInfo.BeatmapSet;
Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
beatmap = new RecyclableLazy<IBeatmap>(() =>
{
var b = GetBeatmap() ?? new Beatmap();
// The original beatmap version needs to be preserved as the database doesn't contain it
BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion;
// Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc)
b.BeatmapInfo = BeatmapInfo;
return b;
});
track = new RecyclableLazy<Track>(() => GetTrack() ?? GetVirtualTrack()); track = new RecyclableLazy<Track>(() => GetTrack() ?? GetVirtualTrack());
background = new RecyclableLazy<Texture>(GetBackground, BackgroundStillValid); background = new RecyclableLazy<Texture>(GetBackground, BackgroundStillValid);
waveform = new RecyclableLazy<Waveform>(GetWaveform); waveform = new RecyclableLazy<Waveform>(GetWaveform);
storyboard = new RecyclableLazy<Storyboard>(GetStoryboard); storyboard = new RecyclableLazy<Storyboard>(GetStoryboard);
skin = new RecyclableLazy<Skin>(GetSkin); skin = new RecyclableLazy<Skin>(GetSkin);
total_count.Value++;
} }
protected virtual Track GetVirtualTrack() protected virtual Track GetVirtualTrack()
@ -153,10 +146,40 @@ namespace osu.Game.Beatmaps
public override string ToString() => BeatmapInfo.ToString(); public override string ToString() => BeatmapInfo.ToString();
public bool BeatmapLoaded => beatmap.IsResultAvailable; public bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
public IBeatmap Beatmap => beatmap.Value;
public Task<IBeatmap> LoadBeatmapAsync() => (beatmapLoadTask ?? (beatmapLoadTask = Task.Factory.StartNew(() =>
{
// Todo: Handle cancellation during beatmap parsing
var b = GetBeatmap() ?? new Beatmap();
// The original beatmap version needs to be preserved as the database doesn't contain it
BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion;
// Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc)
b.BeatmapInfo = BeatmapInfo;
return b;
}, beatmapCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default)));
public IBeatmap Beatmap
{
get
{
try
{
return LoadBeatmapAsync().Result;
}
catch (TaskCanceledException)
{
return null;
}
}
}
private readonly CancellationTokenSource beatmapCancellation = new CancellationTokenSource();
protected abstract IBeatmap GetBeatmap(); protected abstract IBeatmap GetBeatmap();
private readonly RecyclableLazy<IBeatmap> beatmap; private Task<IBeatmap> beatmapLoadTask;
public bool BackgroundLoaded => background.IsResultAvailable; public bool BackgroundLoaded => background.IsResultAvailable;
public Texture Background => background.Value; public Texture Background => background.Value;
@ -195,20 +218,46 @@ namespace osu.Game.Beatmaps
other.track = track; other.track = track;
} }
public virtual void Dispose()
{
background.Recycle();
waveform.Recycle();
storyboard.Recycle();
skin.Recycle();
}
/// <summary> /// <summary>
/// Eagerly dispose of the audio track associated with this <see cref="WorkingBeatmap"/> (if any). /// Eagerly dispose of the audio track associated with this <see cref="WorkingBeatmap"/> (if any).
/// Accessing track again will load a fresh instance. /// Accessing track again will load a fresh instance.
/// </summary> /// </summary>
public virtual void RecycleTrack() => track.Recycle(); public virtual void RecycleTrack() => track.Recycle();
#region Disposal
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private bool isDisposed;
protected virtual void Dispose(bool isDisposing)
{
if (isDisposed)
return;
isDisposed = true;
// recycling logic is not here for the time being, as components which use
// retrieved objects from WorkingBeatmap may not hold a reference to the WorkingBeatmap itself.
// this should be fine as each retrieved component do have their own finalizers.
// cancelling the beatmap load is safe for now since the retrieval is a synchronous
// operation. if we add an async retrieval method this may need to be reconsidered.
beatmapCancellation.Cancel();
total_count.Value--;
}
~WorkingBeatmap()
{
Dispose(false);
}
#endregion
public class RecyclableLazy<T> public class RecyclableLazy<T>
{ {
private Lazy<T> lazy; private Lazy<T> lazy;

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Statistics;
namespace osu.Game.Database namespace osu.Game.Database
{ {
@ -31,11 +32,20 @@ namespace osu.Game.Database
recycleThreadContexts(); recycleThreadContexts();
} }
private static readonly GlobalStatistic<int> reads = GlobalStatistics.Get<int>("Database", "Get (Read)");
private static readonly GlobalStatistic<int> writes = GlobalStatistics.Get<int>("Database", "Get (Write)");
private static readonly GlobalStatistic<int> commits = GlobalStatistics.Get<int>("Database", "Commits");
private static readonly GlobalStatistic<int> rollbacks = GlobalStatistics.Get<int>("Database", "Rollbacks");
/// <summary> /// <summary>
/// Get a context for the current thread for read-only usage. /// Get a context for the current thread for read-only usage.
/// If a <see cref="DatabaseWriteUsage"/> is in progress, the existing write-safe context will be returned. /// If a <see cref="DatabaseWriteUsage"/> is in progress, the existing write-safe context will be returned.
/// </summary> /// </summary>
public OsuDbContext Get() => threadContexts.Value; public OsuDbContext Get()
{
reads.Value++;
return threadContexts.Value;
}
/// <summary> /// <summary>
/// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context).
@ -45,6 +55,7 @@ namespace osu.Game.Database
/// <returns>A usage containing a usable context.</returns> /// <returns>A usage containing a usable context.</returns>
public DatabaseWriteUsage GetForWrite(bool withTransaction = true) public DatabaseWriteUsage GetForWrite(bool withTransaction = true)
{ {
writes.Value++;
Monitor.Enter(writeLock); Monitor.Enter(writeLock);
OsuDbContext context; OsuDbContext context;
@ -90,9 +101,15 @@ namespace osu.Game.Database
if (usages == 0) if (usages == 0)
{ {
if (currentWriteDidError) if (currentWriteDidError)
{
rollbacks.Value++;
currentWriteTransaction?.Rollback(); currentWriteTransaction?.Rollback();
}
else else
{
commits.Value++;
currentWriteTransaction?.Commit(); currentWriteTransaction?.Commit();
}
if (currentWriteDidWrite || currentWriteDidError) if (currentWriteDidWrite || currentWriteDidError)
{ {

View File

@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Statistics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.IO; using osu.Game.IO;
@ -34,6 +35,8 @@ namespace osu.Game.Database
private static readonly Lazy<OsuDbLoggerFactory> logger = new Lazy<OsuDbLoggerFactory>(() => new OsuDbLoggerFactory()); private static readonly Lazy<OsuDbLoggerFactory> logger = new Lazy<OsuDbLoggerFactory>(() => new OsuDbLoggerFactory());
private static readonly GlobalStatistic<int> contexts = GlobalStatistics.Get<int>("Database", "Contexts");
static OsuDbContext() static OsuDbContext()
{ {
// required to initialise native SQLite libraries on some platforms. // required to initialise native SQLite libraries on some platforms.
@ -76,6 +79,8 @@ namespace osu.Game.Database
connection.Close(); connection.Close();
throw; throw;
} }
contexts.Value++;
} }
~OsuDbContext() ~OsuDbContext()
@ -85,6 +90,20 @@ namespace osu.Game.Database
Dispose(); Dispose();
} }
private bool isDisposed;
public override void Dispose()
{
if (isDisposed) return;
isDisposed = true;
base.Dispose();
contexts.Value--;
GC.SuppressFinalize(this);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
base.OnConfiguring(optionsBuilder); base.OnConfiguring(optionsBuilder);

View File

@ -6,23 +6,28 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Graphics.Transforms;
using osuTK;
namespace osu.Game.Graphics.Backgrounds namespace osu.Game.Graphics.Backgrounds
{ {
public class Background : BufferedContainer /// <summary>
/// A background which offers blurring via a <see cref="BufferedContainer"/> on demand.
/// </summary>
public class Background : CompositeDrawable
{ {
public Sprite Sprite; public Sprite Sprite;
private readonly string textureName; private readonly string textureName;
private BufferedContainer bufferedContainer;
public Background(string textureName = @"") public Background(string textureName = @"")
{ {
CacheDrawnFrameBuffer = true;
this.textureName = textureName; this.textureName = textureName;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Add(Sprite = new Sprite AddInternal(Sprite = new Sprite
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -37,5 +42,28 @@ namespace osu.Game.Graphics.Backgrounds
if (!string.IsNullOrEmpty(textureName)) if (!string.IsNullOrEmpty(textureName))
Sprite.Texture = textures.Get(textureName); Sprite.Texture = textures.Get(textureName);
} }
public Vector2 BlurSigma => bufferedContainer?.BlurSigma ?? Vector2.Zero;
/// <summary>
/// Smoothly adjusts <see cref="IBufferedContainer.BlurSigma"/> over time.
/// </summary>
/// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns>
public void BlurTo(Vector2 newBlurSigma, double duration = 0, Easing easing = Easing.None)
{
if (bufferedContainer == null)
{
RemoveInternal(Sprite);
AddInternal(bufferedContainer = new BufferedContainer
{
CacheDrawnFrameBuffer = true,
RelativeSizeAxes = Axes.Both,
Child = Sprite
});
}
bufferedContainer.BlurTo(newBlurSigma, duration, easing);
}
} }
} }

View File

@ -0,0 +1,87 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Online;
using osuTK;
namespace osu.Game.Graphics.UserInterface
{
public class DownloadButton : OsuAnimatedButton
{
public readonly Bindable<DownloadState> State = new Bindable<DownloadState>();
private readonly SpriteIcon icon;
private readonly SpriteIcon checkmark;
private readonly Box background;
private OsuColour colours;
public DownloadButton()
{
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue
},
icon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(13),
Icon = FontAwesome.Solid.Download,
},
checkmark = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
X = 8,
Size = Vector2.Zero,
Icon = FontAwesome.Solid.Check,
}
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
this.colours = colours;
State.BindValueChanged(updateState, true);
}
private void updateState(ValueChangedEvent<DownloadState> state)
{
switch (state.NewValue)
{
case DownloadState.NotDownloaded:
background.FadeColour(colours.Gray4, 500, Easing.InOutExpo);
icon.MoveToX(0, 500, Easing.InOutExpo);
checkmark.ScaleTo(Vector2.Zero, 500, Easing.InOutExpo);
break;
case DownloadState.Downloading:
background.FadeColour(colours.Blue, 500, Easing.InOutExpo);
icon.MoveToX(0, 500, Easing.InOutExpo);
checkmark.ScaleTo(Vector2.Zero, 500, Easing.InOutExpo);
break;
case DownloadState.Downloaded:
background.FadeColour(colours.Yellow, 500, Easing.InOutExpo);
break;
case DownloadState.LocallyAvailable:
background.FadeColour(colours.Green, 500, Easing.InOutExpo);
icon.MoveToX(-8, 500, Easing.InOutExpo);
checkmark.ScaleTo(new Vector2(13), 500, Easing.InOutExpo);
break;
}
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Scoring;
namespace osu.Game.Online.API.Requests
{
public class DownloadReplayRequest : ArchiveDownloadRequest<ScoreInfo>
{
public DownloadReplayRequest(ScoreInfo score)
: base(score)
{
}
protected override string FileExtension => ".osr";
protected override string Target => $@"scores/{Model.Ruleset.ShortName}/{Model.OnlineScoreID}/download";
}
}

View File

@ -32,12 +32,15 @@ namespace osu.Game.Online.API.Requests.Responses
set => User = value; set => User = value;
} }
[JsonProperty(@"score_id")] [JsonProperty(@"id")]
private long onlineScoreID private long onlineScoreID
{ {
set => OnlineScoreID = value; set => OnlineScoreID = value;
} }
[JsonProperty(@"replay")]
public bool Replay { get; set; }
[JsonProperty(@"created_at")] [JsonProperty(@"created_at")]
private DateTimeOffset date private DateTimeOffset date
{ {

View File

@ -54,6 +54,12 @@ namespace osu.Game.Online
attachDownload(download); attachDownload(download);
}; };
manager.DownloadFailed += download =>
{
if (download.Model.Equals(Model.Value))
attachDownload(null);
};
manager.ItemAdded += itemAdded; manager.ItemAdded += itemAdded;
manager.ItemRemoved += itemRemoved; manager.ItemRemoved += itemRemoved;
} }

View File

@ -1,45 +1,131 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics.Sprites; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Sprites;
using osu.Game.Scoring; using osu.Game.Scoring;
using System; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Online.Leaderboards namespace osu.Game.Online.Leaderboards
{ {
public class DrawableRank : Sprite public class DrawableRank : CompositeDrawable
{ {
private readonly ScoreRank rank; private readonly ScoreRank rank;
public DrawableRank(ScoreRank rank) public DrawableRank(ScoreRank rank)
{ {
this.rank = rank; this.rank = rank;
RelativeSizeAxes = Axes.Both;
FillMode = FillMode.Fit;
FillAspectRatio = 2;
var rankColour = getRankColour();
InternalChild = new DrawSizePreservingFillContainer
{
TargetDrawSize = new Vector2(64, 32),
Strategy = DrawSizePreservationStrategy.Minimum,
Child = new CircularContainer
{
Masking = true,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = rankColour,
},
new Triangles
{
RelativeSizeAxes = Axes.Both,
ColourDark = rankColour.Darken(0.1f),
ColourLight = rankColour.Lighten(0.1f),
Velocity = 0.25f,
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(-3, 0),
Padding = new MarginPadding { Top = 5 },
Colour = getRankNameColour(),
Font = OsuFont.GetFont(Typeface.Venera, 25),
Text = getRankName(),
ShadowColour = Color4.Black.Opacity(0.3f),
ShadowOffset = new Vector2(0, 0.08f),
Shadow = true,
},
}
}
};
} }
[BackgroundDependencyLoader(true)] private string getRankName() => rank.GetDescription().TrimEnd('+');
private void load(TextureStore ts)
{
if (ts == null)
throw new ArgumentNullException(nameof(ts));
Texture = ts.Get($@"Grades/{getTextureName()}"); /// <summary>
} /// Retrieves the grade background colour.
/// </summary>
private string getTextureName() private Color4 getRankColour()
{ {
switch (rank) switch (rank)
{ {
default: case ScoreRank.XH:
return rank.GetDescription(); case ScoreRank.X:
return OsuColour.FromHex(@"ce1c9d");
case ScoreRank.SH: case ScoreRank.SH:
return "SPlus"; case ScoreRank.S:
return OsuColour.FromHex(@"00a8b5");
case ScoreRank.A:
return OsuColour.FromHex(@"7cce14");
case ScoreRank.B:
return OsuColour.FromHex(@"e3b130");
case ScoreRank.C:
return OsuColour.FromHex(@"f18252");
default:
return OsuColour.FromHex(@"e95353");
}
}
/// <summary>
/// Retrieves the grade text colour.
/// </summary>
private ColourInfo getRankNameColour()
{
switch (rank)
{
case ScoreRank.XH: case ScoreRank.XH:
return "SSPlus"; case ScoreRank.SH:
return ColourInfo.GradientVertical(Color4.White, OsuColour.FromHex("afdff0"));
case ScoreRank.X:
case ScoreRank.S:
return ColourInfo.GradientVertical(OsuColour.FromHex(@"ffe7a8"), OsuColour.FromHex(@"ffb800"));
case ScoreRank.A:
return OsuColour.FromHex(@"275227");
case ScoreRank.B:
return OsuColour.FromHex(@"553a2b");
case ScoreRank.C:
return OsuColour.FromHex(@"473625");
default:
return OsuColour.FromHex(@"512525");
} }
} }
} }

View File

@ -22,10 +22,8 @@ namespace osu.Game.Online.Leaderboards
protected override Drawable CreateDrawable(ScoreRank rank) => new DrawableRank(rank) protected override Drawable CreateDrawable(ScoreRank rank) => new DrawableRank(rank)
{ {
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
FillMode = FillMode.Fit,
}; };
} }
} }

View File

@ -261,8 +261,10 @@ namespace osu.Game
/// </summary> /// </summary>
public void PresentScore(ScoreInfo score) public void PresentScore(ScoreInfo score)
{ {
var databasedScore = ScoreManager.GetScore(score); // The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database
var databasedScoreInfo = databasedScore.ScoreInfo; // to ensure all the required data for presenting a replay are present.
var databasedScoreInfo = ScoreManager.Query(s => s.OnlineScoreID == score.OnlineScoreID);
var databasedScore = ScoreManager.GetScore(databasedScoreInfo);
if (databasedScore.Replay == null) if (databasedScore.Replay == null)
{ {
@ -295,6 +297,10 @@ namespace osu.Game
var nextBeatmap = beatmap.NewValue; var nextBeatmap = beatmap.NewValue;
if (nextBeatmap?.Track != null) if (nextBeatmap?.Track != null)
nextBeatmap.Track.Completed += currentTrackCompleted; nextBeatmap.Track.Completed += currentTrackCompleted;
beatmap.OldValue?.Dispose();
nextBeatmap?.LoadBeatmapAsync();
} }
private void currentTrackCompleted() private void currentTrackCompleted()

View File

@ -171,7 +171,7 @@ namespace osu.Game
dependencies.Cache(FileStore = new FileStore(contextFactory, Host.Storage)); dependencies.Cache(FileStore = new FileStore(contextFactory, Host.Storage));
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Host.Storage, contextFactory, Host)); dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Host.Storage, API, contextFactory, Host));
dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap)); dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap));
// this should likely be moved to ArchiveModelManager when another case appers where it is necessary // this should likely be moved to ArchiveModelManager when another case appers where it is necessary

View File

@ -20,7 +20,7 @@ using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapSet.Buttons namespace osu.Game.Overlays.BeatmapSet.Buttons
{ {
public class DownloadButton : BeatmapDownloadTrackingComposite, IHasTooltip public class HeaderDownloadButton : BeatmapDownloadTrackingComposite, IHasTooltip
{ {
private readonly bool noVideo; private readonly bool noVideo;
@ -31,7 +31,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
private ShakeContainer shakeContainer; private ShakeContainer shakeContainer;
private HeaderButton button; private HeaderButton button;
public DownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false) public HeaderDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false)
: base(beatmapSet) : base(beatmapSet)
{ {
this.noVideo = noVideo; this.noVideo = noVideo;

View File

@ -18,7 +18,6 @@ using osu.Game.Overlays.BeatmapSet.Buttons;
using osu.Game.Overlays.Direct; using osu.Game.Overlays.Direct;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using DownloadButton = osu.Game.Overlays.BeatmapSet.Buttons.DownloadButton;
namespace osu.Game.Overlays.BeatmapSet namespace osu.Game.Overlays.BeatmapSet
{ {
@ -268,7 +267,7 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
case DownloadState.LocallyAvailable: case DownloadState.LocallyAvailable:
// temporary for UX until new design is implemented. // temporary for UX until new design is implemented.
downloadButtonsContainer.Child = new Direct.DownloadButton(BeatmapSet.Value) downloadButtonsContainer.Child = new PanelDownloadButton(BeatmapSet.Value)
{ {
Width = 50, Width = 50,
RelativeSizeAxes = Axes.Y RelativeSizeAxes = Axes.Y
@ -278,13 +277,13 @@ namespace osu.Game.Overlays.BeatmapSet
case DownloadState.Downloading: case DownloadState.Downloading:
case DownloadState.Downloaded: case DownloadState.Downloaded:
// temporary to avoid showing two buttons for maps with novideo. will be fixed in new beatmap overlay design. // temporary to avoid showing two buttons for maps with novideo. will be fixed in new beatmap overlay design.
downloadButtonsContainer.Child = new DownloadButton(BeatmapSet.Value); downloadButtonsContainer.Child = new HeaderDownloadButton(BeatmapSet.Value);
break; break;
default: default:
downloadButtonsContainer.Child = new DownloadButton(BeatmapSet.Value); downloadButtonsContainer.Child = new HeaderDownloadButton(BeatmapSet.Value);
if (BeatmapSet.Value.OnlineInfo.HasVideo) if (BeatmapSet.Value.OnlineInfo.HasVideo)
downloadButtonsContainer.Add(new DownloadButton(BeatmapSet.Value, true)); downloadButtonsContainer.Add(new HeaderDownloadButton(BeatmapSet.Value, true));
break; break;
} }
} }

View File

@ -155,7 +155,7 @@ namespace osu.Game.Overlays.Direct
}, },
}, },
}, },
new DownloadButton(SetInfo) new PanelDownloadButton(SetInfo)
{ {
Size = new Vector2(50, 30), Size = new Vector2(50, 30),
Margin = new MarginPadding(horizontal_padding), Margin = new MarginPadding(horizontal_padding),

View File

@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Direct
private const float height = 70; private const float height = 70;
private FillFlowContainer statusContainer; private FillFlowContainer statusContainer;
protected DownloadButton DownloadButton; protected PanelDownloadButton DownloadButton;
private PlayButton playButton; private PlayButton playButton;
private Box progressBar; private Box progressBar;
@ -150,7 +150,7 @@ namespace osu.Game.Overlays.Direct
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Child = DownloadButton = new DownloadButton(SetInfo) Child = DownloadButton = new PanelDownloadButton(SetInfo)
{ {
Size = new Vector2(height - vertical_padding * 3), Size = new Vector2(height - vertical_padding * 3),
Margin = new MarginPadding { Left = vertical_padding * 2, Right = vertical_padding }, Margin = new MarginPadding { Left = vertical_padding * 2, Right = vertical_padding },

View File

@ -1,136 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osuTK;
namespace osu.Game.Overlays.Direct
{
public class DownloadButton : BeatmapDownloadTrackingComposite
{
protected bool DownloadEnabled => button.Enabled.Value;
private readonly bool noVideo;
private readonly SpriteIcon icon;
private readonly SpriteIcon checkmark;
private readonly Box background;
private OsuColour colours;
private readonly ShakeContainer shakeContainer;
private readonly OsuAnimatedButton button;
public DownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false)
: base(beatmapSet)
{
this.noVideo = noVideo;
InternalChild = shakeContainer = new ShakeContainer
{
RelativeSizeAxes = Axes.Both,
Child = button = new OsuAnimatedButton
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue
},
icon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(13),
Icon = FontAwesome.Solid.Download,
},
checkmark = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
X = 8,
Size = Vector2.Zero,
Icon = FontAwesome.Solid.Check,
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
State.BindValueChanged(state => updateState(state.NewValue), true);
FinishTransforms(true);
}
[BackgroundDependencyLoader(true)]
private void load(OsuColour colours, OsuGame game, BeatmapManager beatmaps)
{
this.colours = colours;
if (BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false)
{
button.Enabled.Value = false;
button.TooltipText = "This beatmap is currently not available for download.";
return;
}
button.Action = () =>
{
switch (State.Value)
{
case DownloadState.Downloading:
case DownloadState.Downloaded:
shakeContainer.Shake();
break;
case DownloadState.LocallyAvailable:
game.PresentBeatmap(BeatmapSet.Value);
break;
default:
beatmaps.Download(BeatmapSet.Value, noVideo);
break;
}
};
}
private void updateState(DownloadState state)
{
switch (state)
{
case DownloadState.NotDownloaded:
background.FadeColour(colours.Gray4, 500, Easing.InOutExpo);
icon.MoveToX(0, 500, Easing.InOutExpo);
checkmark.ScaleTo(Vector2.Zero, 500, Easing.InOutExpo);
break;
case DownloadState.Downloading:
background.FadeColour(colours.Blue, 500, Easing.InOutExpo);
icon.MoveToX(0, 500, Easing.InOutExpo);
checkmark.ScaleTo(Vector2.Zero, 500, Easing.InOutExpo);
break;
case DownloadState.Downloaded:
background.FadeColour(colours.Yellow, 500, Easing.InOutExpo);
break;
case DownloadState.LocallyAvailable:
background.FadeColour(colours.Green, 500, Easing.InOutExpo);
icon.MoveToX(-8, 500, Easing.InOutExpo);
checkmark.ScaleTo(new Vector2(13), 500, Easing.InOutExpo);
break;
}
}
}
}

View File

@ -0,0 +1,75 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
namespace osu.Game.Overlays.Direct
{
public class PanelDownloadButton : BeatmapDownloadTrackingComposite
{
protected bool DownloadEnabled => button.Enabled.Value;
private readonly bool noVideo;
private readonly ShakeContainer shakeContainer;
private readonly DownloadButton button;
public PanelDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false)
: base(beatmapSet)
{
this.noVideo = noVideo;
InternalChild = shakeContainer = new ShakeContainer
{
RelativeSizeAxes = Axes.Both,
Child = button = new DownloadButton
{
RelativeSizeAxes = Axes.Both,
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
button.State.BindTo(State);
FinishTransforms(true);
}
[BackgroundDependencyLoader(true)]
private void load(OsuGame game, BeatmapManager beatmaps)
{
if (BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false)
{
button.Enabled.Value = false;
button.TooltipText = "This beatmap is currently not available for download.";
return;
}
button.Action = () =>
{
switch (State.Value)
{
case DownloadState.Downloading:
case DownloadState.Downloaded:
shakeContainer.Shake();
break;
case DownloadState.LocallyAvailable:
game.PresentBeatmap(BeatmapSet.Value);
break;
default:
beatmaps.Download(BeatmapSet.Value, noVideo);
break;
}
};
}
}
}

View File

@ -72,18 +72,30 @@ namespace osu.Game.Overlays.Profile.Header
new FillFlowContainer new FillFlowContainer
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal, Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
usernameText = new OsuSpriteText new FillFlowContainer
{ {
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular) AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
usernameText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
},
openUserExternally = new ExternalLinkButton
{
Margin = new MarginPadding { Left = 5 },
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
}, },
openUserExternally = new ExternalLinkButton titleText = new OsuSpriteText
{ {
Margin = new MarginPadding { Left = 5 }, Font = OsuFont.GetFont(size: 18, weight: FontWeight.Regular)
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}, },
} }
}, },
@ -95,10 +107,6 @@ namespace osu.Game.Overlays.Profile.Header
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
titleText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Regular)
},
supporterTag = new SupporterIcon supporterTag = new SupporterIcon
{ {
Height = 20, Height = 20,
@ -111,10 +119,11 @@ namespace osu.Game.Overlays.Profile.Header
Margin = new MarginPadding { Top = 10 }, Margin = new MarginPadding { Top = 10 },
Colour = colours.GreySeafoamLighter, Colour = colours.GreySeafoamLighter,
}, },
new Container new FillFlowContainer
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Top = 5 }, Margin = new MarginPadding { Top = 5 },
Direction = FillDirection.Horizontal,
Children = new Drawable[] Children = new Drawable[]
{ {
userFlag = new UpdateableFlag userFlag = new UpdateableFlag
@ -125,7 +134,7 @@ namespace osu.Game.Overlays.Profile.Header
userCountryText = new OsuSpriteText userCountryText = new OsuSpriteText
{ {
Font = OsuFont.GetFont(size: 17.5f, weight: FontWeight.Regular), Font = OsuFont.GetFont(size: 17.5f, weight: FontWeight.Regular),
Margin = new MarginPadding { Left = 40 }, Margin = new MarginPadding { Left = 10 },
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Colour = colours.GreySeafoamLighter, Colour = colours.GreySeafoamLighter,

View File

@ -66,24 +66,64 @@ namespace osu.Game.Overlays
} }
}; };
Header.Tabs.Current.ValueChanged += _ => Scheduler.AddOnce(updateSearch); Header.Tabs.Current.ValueChanged += _ => queueUpdate();
Filter.Tabs.Current.ValueChanged += _ => Scheduler.AddOnce(updateSearch); Filter.Tabs.Current.ValueChanged += _ => queueUpdate();
Filter.DisplayStyleControl.DisplayStyle.ValueChanged += style => recreatePanels(style.NewValue); Filter.DisplayStyleControl.DisplayStyle.ValueChanged += style => recreatePanels(style.NewValue);
Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => Scheduler.AddOnce(updateSearch); Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => queueUpdate();
currentQuery.BindTo(Filter.Search.Current);
currentQuery.ValueChanged += query => currentQuery.ValueChanged += query =>
{ {
queryChangedDebounce?.Cancel(); queryChangedDebounce?.Cancel();
if (string.IsNullOrEmpty(query.NewValue)) if (string.IsNullOrEmpty(query.NewValue))
Scheduler.AddOnce(updateSearch); queueUpdate();
else else
queryChangedDebounce = Scheduler.AddDelayed(updateSearch, 500); queryChangedDebounce = Scheduler.AddDelayed(updateSearch, 500);
}; };
}
currentQuery.BindTo(Filter.Search.Current); private APIRequest getUsersRequest;
private readonly Bindable<string> currentQuery = new Bindable<string>();
private ScheduledDelegate queryChangedDebounce;
private void queueUpdate() => Scheduler.AddOnce(updateSearch);
private void updateSearch()
{
queryChangedDebounce?.Cancel();
if (!IsLoaded)
return;
Users = null;
clearPanels();
loading.Hide();
getUsersRequest?.Cancel();
if (API?.IsLoggedIn != true)
return;
switch (Header.Tabs.Current.Value)
{
case SocialTab.Friends:
var friendRequest = new GetFriendsRequest(); // TODO filter arguments?
friendRequest.Success += updateUsers;
API.Queue(getUsersRequest = friendRequest);
break;
default:
var userRequest = new GetUsersRequest(); // TODO filter arguments!
userRequest.Success += response => updateUsers(response.Select(r => r.User));
API.Queue(getUsersRequest = userRequest);
break;
}
loading.Show();
} }
private void recreatePanels(PanelDisplayStyle displayStyle) private void recreatePanels(PanelDisplayStyle displayStyle)
@ -133,45 +173,6 @@ namespace osu.Game.Overlays
}); });
} }
private APIRequest getUsersRequest;
private readonly Bindable<string> currentQuery = new Bindable<string>();
private ScheduledDelegate queryChangedDebounce;
private void updateSearch()
{
queryChangedDebounce?.Cancel();
if (!IsLoaded)
return;
Users = null;
clearPanels();
loading.Hide();
getUsersRequest?.Cancel();
if (API?.IsLoggedIn != true)
return;
switch (Header.Tabs.Current.Value)
{
case SocialTab.Friends:
var friendRequest = new GetFriendsRequest(); // TODO filter arguments?
friendRequest.Success += updateUsers;
API.Queue(getUsersRequest = friendRequest);
break;
default:
var userRequest = new GetUsersRequest(); // TODO filter arguments!
userRequest.Success += response => updateUsers(response.Select(r => r.User));
API.Queue(getUsersRequest = userRequest);
break;
}
loading.Show();
}
private void updateUsers(IEnumerable<User> newUsers) private void updateUsers(IEnumerable<User> newUsers)
{ {
Users = newUsers; Users = newUsers;
@ -193,7 +194,7 @@ namespace osu.Game.Overlays
switch (state) switch (state)
{ {
case APIState.Online: case APIState.Online:
Scheduler.AddOnce(updateSearch); queueUpdate();
break; break;
default: default:

View File

@ -22,9 +22,11 @@ namespace osu.Game.Rulesets
{ {
AppDomain.CurrentDomain.AssemblyResolve += currentDomain_AssemblyResolve; AppDomain.CurrentDomain.AssemblyResolve += currentDomain_AssemblyResolve;
foreach (string file in Directory.GetFiles(Environment.CurrentDirectory, $"{ruleset_library_prefix}.*.dll") // On android in release configuration assemblies are loaded from the apk directly into memory.
.Where(f => !Path.GetFileName(f).Contains("Tests"))) // We cannot read assemblies from cwd, so should check loaded assemblies instead.
loadRulesetFromFile(file); loadFromAppDomain();
loadFromDisk();
} }
public RulesetStore(IDatabaseContextFactory factory) public RulesetStore(IDatabaseContextFactory factory)
@ -111,6 +113,34 @@ namespace osu.Game.Rulesets
} }
} }
private static void loadFromAppDomain()
{
foreach (var ruleset in AppDomain.CurrentDomain.GetAssemblies())
{
string rulesetName = ruleset.GetName().Name;
if (!rulesetName.StartsWith(ruleset_library_prefix, StringComparison.InvariantCultureIgnoreCase) || ruleset.GetName().Name.Contains("Tests"))
continue;
addRuleset(ruleset);
}
}
private static void loadFromDisk()
{
try
{
string[] files = Directory.GetFiles(Environment.CurrentDirectory, $"{ruleset_library_prefix}.*.dll");
foreach (string file in files.Where(f => !Path.GetFileName(f).Contains("Tests")))
loadRulesetFromFile(file);
}
catch
{
Logger.Log($"Could not load rulesets from directory {Environment.CurrentDirectory}");
}
}
private static void loadRulesetFromFile(string file) private static void loadRulesetFromFile(string file)
{ {
var filename = Path.GetFileNameWithoutExtension(file); var filename = Path.GetFileNameWithoutExtension(file);
@ -120,13 +150,27 @@ namespace osu.Game.Rulesets
try try
{ {
var assembly = Assembly.LoadFrom(file); addRuleset(Assembly.LoadFrom(file));
loaded_assemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset)));
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Error(e, $"Failed to load ruleset {filename}"); Logger.Error(e, $"Failed to load ruleset {filename}");
} }
} }
private static void addRuleset(Assembly assembly)
{
if (loaded_assemblies.ContainsKey(assembly))
return;
try
{
loaded_assemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset)));
}
catch (Exception e)
{
Logger.Error(e, $"Failed to add ruleset {assembly}");
}
}
} }
} }

View File

@ -16,7 +16,7 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Scoring namespace osu.Game.Scoring
{ {
public class ScoreInfo : IHasFiles<ScoreFileInfo>, IHasPrimaryKey, ISoftDelete public class ScoreInfo : IHasFiles<ScoreFileInfo>, IHasPrimaryKey, ISoftDelete, IEquatable<ScoreInfo>
{ {
public int ID { get; set; } public int ID { get; set; }
@ -182,5 +182,7 @@ namespace osu.Game.Scoring
} }
public override string ToString() => $"{User} playing {Beatmap}"; public override string ToString() => $"{User} playing {Beatmap}";
public bool Equals(ScoreInfo other) => other?.OnlineScoreID == OnlineScoreID;
} }
} }

View File

@ -11,12 +11,14 @@ using osu.Framework.Platform;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Scoring.Legacy; using osu.Game.Scoring.Legacy;
namespace osu.Game.Scoring namespace osu.Game.Scoring
{ {
public class ScoreManager : ArchiveModelManager<ScoreInfo, ScoreFileInfo> public class ScoreManager : DownloadableArchiveModelManager<ScoreInfo, ScoreFileInfo>
{ {
public override string[] HandledExtensions => new[] { ".osr" }; public override string[] HandledExtensions => new[] { ".osr" };
@ -27,8 +29,8 @@ namespace osu.Game.Scoring
private readonly RulesetStore rulesets; private readonly RulesetStore rulesets;
private readonly Func<BeatmapManager> beatmaps; private readonly Func<BeatmapManager> beatmaps;
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, IDatabaseContextFactory contextFactory, IIpcHost importHost = null) public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, IIpcHost importHost = null)
: base(storage, contextFactory, new ScoreStore(contextFactory, storage), importHost) : base(storage, contextFactory, api, new ScoreStore(contextFactory, storage), importHost)
{ {
this.rulesets = rulesets; this.rulesets = rulesets;
this.beatmaps = beatmaps; this.beatmaps = beatmaps;
@ -60,5 +62,9 @@ namespace osu.Game.Scoring
public IEnumerable<ScoreInfo> QueryScores(Expression<Func<ScoreInfo, bool>> query) => ModelStore.ConsumableItems.AsNoTracking().Where(query); public IEnumerable<ScoreInfo> QueryScores(Expression<Func<ScoreInfo, bool>> query) => ModelStore.ConsumableItems.AsNoTracking().Where(query);
public ScoreInfo Query(Expression<Func<ScoreInfo, bool>> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query); public ScoreInfo Query(Expression<Func<ScoreInfo, bool>> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query);
protected override ArchiveDownloadRequest<ScoreInfo> CreateDownloadRequest(ScoreInfo score, bool minimiseDownload) => new DownloadReplayRequest(score);
protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable<ScoreInfo> items) => items.Any(s => s.OnlineScoreID == model.OnlineScoreID);
} }
} }

View File

@ -18,7 +18,7 @@ namespace osu.Game.Screens.Backgrounds
private Background background; private Background background;
private int currentDisplay; private int currentDisplay;
private const int background_count = 5; private const int background_count = 7;
private string backgroundName => $@"Menu/menu-background-{currentDisplay % background_count + 1}"; private string backgroundName => $@"Menu/menu-background-{currentDisplay % background_count + 1}";

View File

@ -86,6 +86,7 @@ namespace osu.Game.Screens.Menu
if (!resuming) if (!resuming)
{ {
Beatmap.Value = introBeatmap; Beatmap.Value = introBeatmap;
introBeatmap = null;
if (menuVoice.Value) if (menuVoice.Value)
welcome.Play(); welcome.Play();
@ -94,7 +95,10 @@ namespace osu.Game.Screens.Menu
{ {
// Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Manu. // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Manu.
if (menuMusic.Value) if (menuMusic.Value)
{
track.Start(); track.Start();
track = null;
}
LoadComponentAsync(mainMenu = new MainMenu()); LoadComponentAsync(mainMenu = new MainMenu());

View File

@ -96,13 +96,13 @@ namespace osu.Game.Screens.Menu
var track = beatmap.Value.TrackLoaded ? beatmap.Value.Track : null; var track = beatmap.Value.TrackLoaded ? beatmap.Value.Track : null;
var effect = beatmap.Value.BeatmapLoaded ? beatmap.Value.Beatmap.ControlPointInfo.EffectPointAt(track?.CurrentTime ?? Time.Current) : null; var effect = beatmap.Value.BeatmapLoaded ? beatmap.Value.Beatmap.ControlPointInfo.EffectPointAt(track?.CurrentTime ?? Time.Current) : null;
float[] temporalAmplitudes = track?.CurrentAmplitudes.FrequencyAmplitudes ?? new float[256]; float[] temporalAmplitudes = track?.CurrentAmplitudes.FrequencyAmplitudes;
for (int i = 0; i < bars_per_visualiser; i++) for (int i = 0; i < bars_per_visualiser; i++)
{ {
if (track?.IsRunning ?? false) if (track?.IsRunning ?? false)
{ {
float targetAmplitude = temporalAmplitudes[(i + indexOffset) % bars_per_visualiser] * (effect?.KiaiMode == true ? 1 : 0.5f); float targetAmplitude = (temporalAmplitudes?[(i + indexOffset) % bars_per_visualiser] ?? 0) * (effect?.KiaiMode == true ? 1 : 0.5f);
if (targetAmplitude > frequencyAmplitudes[i]) if (targetAmplitude > frequencyAmplitudes[i])
frequencyAmplitudes[i] = targetAmplitude; frequencyAmplitudes[i] = targetAmplitude;
} }
@ -115,7 +115,6 @@ namespace osu.Game.Screens.Menu
} }
indexOffset = (indexOffset + index_change) % bars_per_visualiser; indexOffset = (indexOffset + index_change) % bars_per_visualiser;
Scheduler.AddDelayed(updateAmplitudes, time_between_updates);
} }
private void updateColour() private void updateColour()
@ -131,7 +130,8 @@ namespace osu.Game.Screens.Menu
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
updateAmplitudes();
Scheduler.AddDelayed(updateAmplitudes, time_between_updates, true);
} }
protected override void Update() protected override void Update()

View File

@ -0,0 +1,103 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online;
using osu.Game.Scoring;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Play
{
public class ReplayDownloadButton : DownloadTrackingComposite<ScoreInfo, ScoreManager>
{
private DownloadButton button;
private ShakeContainer shakeContainer;
private ReplayAvailability replayAvailability
{
get
{
if (State.Value == DownloadState.LocallyAvailable)
return ReplayAvailability.Local;
if (Model.Value is APILegacyScoreInfo apiScore && apiScore.Replay)
return ReplayAvailability.Online;
return ReplayAvailability.NotAvailable;
}
}
public ReplayDownloadButton(ScoreInfo score)
: base(score)
{
}
[BackgroundDependencyLoader(true)]
private void load(OsuGame game, ScoreManager scores)
{
InternalChild = shakeContainer = new ShakeContainer
{
RelativeSizeAxes = Axes.Both,
Child = button = new DownloadButton
{
RelativeSizeAxes = Axes.Both,
}
};
button.Action = () =>
{
switch (State.Value)
{
case DownloadState.LocallyAvailable:
game?.PresentScore(Model.Value);
break;
case DownloadState.NotDownloaded:
scores.Download(Model.Value);
break;
case DownloadState.Downloaded:
case DownloadState.Downloading:
shakeContainer.Shake();
break;
}
};
State.BindValueChanged(state =>
{
button.State.Value = state.NewValue;
switch (replayAvailability)
{
case ReplayAvailability.Local:
button.TooltipText = @"Watch replay";
break;
case ReplayAvailability.Online:
button.TooltipText = @"Download replay";
break;
default:
button.TooltipText = @"Replay unavailable";
break;
}
}, true);
if (replayAvailability == ReplayAvailability.NotAvailable)
{
button.Enabled.Value = false;
button.Alpha = 0.6f;
}
}
private enum ReplayAvailability
{
Local,
Online,
NotAvailable,
}
}
}

View File

@ -33,9 +33,12 @@ namespace osu.Game.Screens.Ranking.Pages
private Container scoreContainer; private Container scoreContainer;
private ScoreCounter scoreCounter; private ScoreCounter scoreCounter;
private readonly ScoreInfo score;
public ScoreResultsPage(ScoreInfo score, WorkingBeatmap beatmap) public ScoreResultsPage(ScoreInfo score, WorkingBeatmap beatmap)
: base(score, beatmap) : base(score, beatmap)
{ {
this.score = score;
} }
private FillFlowContainer<DrawableScoreStatistic> statisticsContainer; private FillFlowContainer<DrawableScoreStatistic> statisticsContainer;
@ -163,9 +166,16 @@ namespace osu.Game.Screens.Ranking.Pages
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
LayoutDuration = 200, LayoutDuration = 200,
LayoutEasing = Easing.OutQuint LayoutEasing = Easing.OutQuint
} },
} },
} },
new ReplayDownloadButton(score)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Margin = new MarginPadding { Bottom = 10 },
Size = new Vector2(50, 30),
},
}; };
statisticsContainer.ChildrenEnumerable = Score.Statistics.OrderByDescending(p => p.Key).Select(s => new DrawableScoreStatistic(s)); statisticsContainer.ChildrenEnumerable = Score.Statistics.OrderByDescending(p => p.Key).Select(s => new DrawableScoreStatistic(s));

View File

@ -137,9 +137,9 @@ namespace osu.Game.Tests.Visual
track = audio?.Tracks.GetVirtual(length); track = audio?.Tracks.GetVirtual(length);
} }
public override void Dispose() protected override void Dispose(bool isDisposing)
{ {
base.Dispose(); base.Dispose(isDisposing);
store?.Dispose(); store?.Dispose();
} }

View File

@ -14,8 +14,8 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.4" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.701.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.702.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.702.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.703.0" />
<PackageReference Include="SharpCompress" Version="0.23.0" /> <PackageReference Include="SharpCompress" Version="0.23.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />

View File

@ -104,9 +104,9 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.701.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.702.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.702.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.703.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2019.702.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2019.703.0" />
<PackageReference Include="SharpCompress" Version="0.22.0" /> <PackageReference Include="SharpCompress" Version="0.22.0" />
<PackageReference Include="NUnit" Version="3.11.0" /> <PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />