mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 08:27:49 +08:00
Merge branch 'master' into alias-author-creator
This commit is contained in:
commit
5d81d83fb2
@ -68,6 +68,7 @@ Aside from the above, below is a brief checklist of things to watch out when you
|
||||
- Please do not make code changes via the GitHub web interface.
|
||||
- Please add tests for your changes. We expect most new features and bugfixes to have test coverage, unless the effort of adding them is prohibitive. The visual testing methodology we use is described in more detail [here](https://github.com/ppy/osu-framework/wiki/Development-and-Testing).
|
||||
- Please run tests and code style analysis (via `InspectCode.{ps1,sh}` scripts in the root of this repository) before opening the PR. This is particularly important if you're a first-time contributor, as CI will not run for your PR until we allow it to do so.
|
||||
- **Do not run the game in release configuration at any point during your testing** (the sole exception to this being benchmarks). Using release is an unnecessary and harmful practice, and can even lead to you losing your local realm database if you start making changes to the schema. The debug configuration has a completely separated full-stack environment, including a development website instance at https://dev.ppy.sh/. It is permitted to register an account on that development instance for testing purposes and not worry about multi-accounting infractions.
|
||||
|
||||
After you're done with your changes and you wish to open the PR, please observe the following recommendations:
|
||||
|
||||
|
@ -124,6 +124,113 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count == 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestControlClickAddsControlPointsIfSingleSliderSelected()
|
||||
{
|
||||
var firstSlider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(0, 0),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
}
|
||||
}
|
||||
};
|
||||
var secondSlider = new Slider
|
||||
{
|
||||
StartTime = 1000,
|
||||
Position = new Vector2(200, 200),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100, -100))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddStep("add objects", () => EditorBeatmap.AddRange(new HitObject[] { firstSlider, secondSlider }));
|
||||
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.AddRange(new HitObject[] { secondSlider }));
|
||||
|
||||
AddStep("move mouse to middle of slider", () =>
|
||||
{
|
||||
var pos = blueprintContainer.SelectionBlueprints
|
||||
.First(s => s.Item == secondSlider)
|
||||
.ChildrenOfType<SliderBodyPiece>().First()
|
||||
.ScreenSpaceDrawQuad.Centre;
|
||||
|
||||
InputManager.MoveMouseTo(pos);
|
||||
});
|
||||
AddStep("control-click left mouse", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1));
|
||||
AddAssert("slider has 3 anchors", () => secondSlider.Path.ControlPoints.Count, () => Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestControlClickDoesNotAddSliderControlPointsIfMultipleObjectsSelected()
|
||||
{
|
||||
var firstSlider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(0, 0),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
}
|
||||
}
|
||||
};
|
||||
var secondSlider = new Slider
|
||||
{
|
||||
StartTime = 1000,
|
||||
Position = new Vector2(200, 200),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100, -100))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddStep("add objects", () => EditorBeatmap.AddRange(new HitObject[] { firstSlider, secondSlider }));
|
||||
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.AddRange(new HitObject[] { firstSlider, secondSlider }));
|
||||
|
||||
AddStep("move mouse to middle of slider", () =>
|
||||
{
|
||||
var pos = blueprintContainer.SelectionBlueprints
|
||||
.First(s => s.Item == secondSlider)
|
||||
.ChildrenOfType<SliderBodyPiece>().First()
|
||||
.ScreenSpaceDrawQuad.Centre;
|
||||
|
||||
InputManager.MoveMouseTo(pos);
|
||||
});
|
||||
AddStep("control-click left mouse", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddAssert("selection not preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1));
|
||||
AddAssert("second slider not selected",
|
||||
() => blueprintContainer.SelectionBlueprints.First(s => s.Item == secondSlider).IsSelected,
|
||||
() => Is.False);
|
||||
AddAssert("slider still has 2 anchors", () => secondSlider.Path.ControlPoints.Count, () => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
private ComposeBlueprintContainer blueprintContainer
|
||||
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
|
||||
|
||||
|
@ -171,7 +171,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
return false; // Allow right click to be handled by context menu
|
||||
|
||||
case MouseButton.Left:
|
||||
if (e.ControlPressed && IsSelected)
|
||||
// If there's more than two objects selected, ctrl+click should deselect
|
||||
if (e.ControlPressed && IsSelected && selectedObjects.Count < 2)
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
placementControlPoint = addControlPoint(e.MousePosition);
|
||||
|
197
osu.Game.Tests/Beatmaps/BeatmapUpdaterMetadataLookupTest.cs
Normal file
197
osu.Game.Tests/Beatmaps/BeatmapUpdaterMetadataLookupTest.cs
Normal file
@ -0,0 +1,197 @@
|
||||
// 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 Moq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps
|
||||
{
|
||||
[TestFixture]
|
||||
public class BeatmapUpdaterMetadataLookupTest
|
||||
{
|
||||
private Mock<IOnlineBeatmapMetadataSource> apiMetadataSourceMock = null!;
|
||||
private Mock<IOnlineBeatmapMetadataSource> localCachedMetadataSourceMock = null!;
|
||||
|
||||
private BeatmapUpdaterMetadataLookup metadataLookup = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
apiMetadataSourceMock = new Mock<IOnlineBeatmapMetadataSource>();
|
||||
localCachedMetadataSourceMock = new Mock<IOnlineBeatmapMetadataSource>();
|
||||
|
||||
metadataLookup = new BeatmapUpdaterMetadataLookup(apiMetadataSourceMock.Object, localCachedMetadataSourceMock.Object);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLocalCacheQueriedFirst()
|
||||
{
|
||||
var localLookupResult = new OnlineBeatmapMetadata { BeatmapID = 123456, BeatmapStatus = BeatmapOnlineStatus.Ranked };
|
||||
localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out localLookupResult))
|
||||
.Returns(true);
|
||||
|
||||
apiMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
|
||||
var beatmap = new BeatmapInfo { OnlineID = 123456 };
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch: false);
|
||||
|
||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
|
||||
localCachedMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata>.IsAny!), Times.Once);
|
||||
apiMetadataSourceMock.Verify(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out It.Ref<OnlineBeatmapMetadata>.IsAny!), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAPIQueriedSecond()
|
||||
{
|
||||
OnlineBeatmapMetadata? localLookupResult = null;
|
||||
localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out localLookupResult))
|
||||
.Returns(false);
|
||||
|
||||
var onlineLookupResult = new OnlineBeatmapMetadata { BeatmapID = 123456, BeatmapStatus = BeatmapOnlineStatus.Ranked };
|
||||
apiMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
apiMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out onlineLookupResult))
|
||||
.Returns(true);
|
||||
|
||||
var beatmap = new BeatmapInfo { OnlineID = 123456 };
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch: false);
|
||||
|
||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
|
||||
localCachedMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata?>.IsAny!), Times.Once);
|
||||
apiMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata?>.IsAny!), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPreferOnlineFetch()
|
||||
{
|
||||
var localLookupResult = new OnlineBeatmapMetadata { BeatmapID = 123456, BeatmapStatus = BeatmapOnlineStatus.Ranked };
|
||||
localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out localLookupResult))
|
||||
.Returns(true);
|
||||
|
||||
var onlineLookupResult = new OnlineBeatmapMetadata { BeatmapID = 123456, BeatmapStatus = BeatmapOnlineStatus.Graveyard };
|
||||
apiMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
apiMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out onlineLookupResult))
|
||||
.Returns(true);
|
||||
|
||||
var beatmap = new BeatmapInfo { OnlineID = 123456 };
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch: true);
|
||||
|
||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Graveyard));
|
||||
localCachedMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata?>.IsAny!), Times.Never);
|
||||
apiMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata?>.IsAny!), Times.Once);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPreferOnlineFetchFallsBackToLocalCacheIfOnlineSourceUnavailable()
|
||||
{
|
||||
var localLookupResult = new OnlineBeatmapMetadata { BeatmapID = 123456, BeatmapStatus = BeatmapOnlineStatus.Ranked };
|
||||
localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out localLookupResult))
|
||||
.Returns(true);
|
||||
|
||||
apiMetadataSourceMock.Setup(src => src.Available).Returns(false);
|
||||
|
||||
var beatmap = new BeatmapInfo { OnlineID = 123456 };
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch: true);
|
||||
|
||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
|
||||
localCachedMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata?>.IsAny!), Times.Once);
|
||||
apiMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata?>.IsAny!), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMetadataLookupFailed()
|
||||
{
|
||||
OnlineBeatmapMetadata? lookupResult = null;
|
||||
|
||||
localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out lookupResult))
|
||||
.Returns(false);
|
||||
|
||||
apiMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
apiMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out lookupResult))
|
||||
.Returns(true);
|
||||
|
||||
var beatmap = new BeatmapInfo { OnlineID = 123456 };
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch: false);
|
||||
|
||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
||||
Assert.That(beatmap.OnlineID, Is.EqualTo(-1));
|
||||
localCachedMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata?>.IsAny!), Times.Once);
|
||||
apiMetadataSourceMock.Verify(src => src.TryLookup(beatmap, out It.Ref<OnlineBeatmapMetadata?>.IsAny!), Times.Once);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// For the time being, if we fail to find a match in the local cache but online retrieval is not available, we trust the incoming beatmap verbatim wrt online ID.
|
||||
/// While this is suboptimal as it implicitly trusts the contents of the beatmap,
|
||||
/// throwing away the online data would be anti-user as it would make all beatmaps imported offline stop working in online.
|
||||
/// TODO: revisit if/when we have a better flow of queueing metadata retrieval.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void TestLocalMetadataLookupReturnedNoMatchAndOnlineLookupIsUnavailable([Values] bool preferOnlineFetch)
|
||||
{
|
||||
OnlineBeatmapMetadata? localLookupResult = null;
|
||||
localCachedMetadataSourceMock.Setup(src => src.Available).Returns(true);
|
||||
localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out localLookupResult))
|
||||
.Returns(false);
|
||||
|
||||
apiMetadataSourceMock.Setup(src => src.Available).Returns(false);
|
||||
|
||||
var beatmap = new BeatmapInfo { OnlineID = 123456 };
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch);
|
||||
|
||||
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
|
||||
Assert.That(beatmap.OnlineID, Is.EqualTo(123456));
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// For the time being, if there are no available metadata lookup sources, we trust the incoming beatmap verbatim wrt online ID.
|
||||
/// While this is suboptimal as it implicitly trusts the contents of the beatmap,
|
||||
/// throwing away the online data would be anti-user as it would make all beatmaps imported offline stop working in online.
|
||||
/// TODO: revisit if/when we have a better flow of queueing metadata retrieval.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void TestNoAvailableSources([Values] bool preferOnlineFetch)
|
||||
{
|
||||
OnlineBeatmapMetadata? lookupResult = null;
|
||||
|
||||
localCachedMetadataSourceMock.Setup(src => src.Available).Returns(false);
|
||||
localCachedMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out lookupResult))
|
||||
.Returns(false);
|
||||
|
||||
apiMetadataSourceMock.Setup(src => src.Available).Returns(false);
|
||||
apiMetadataSourceMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out lookupResult))
|
||||
.Returns(false);
|
||||
|
||||
var beatmap = new BeatmapInfo { OnlineID = 123456 };
|
||||
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
|
||||
beatmap.BeatmapSet = beatmapSet;
|
||||
|
||||
metadataLookup.Update(beatmapSet, preferOnlineFetch);
|
||||
|
||||
Assert.That(beatmap.OnlineID, Is.EqualTo(123456));
|
||||
}
|
||||
}
|
||||
}
|
@ -206,6 +206,12 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Total = 50
|
||||
},
|
||||
SupportLevel = 2,
|
||||
Location = "Somewhere",
|
||||
Interests = "Rhythm games",
|
||||
Occupation = "Gamer",
|
||||
Twitter = "test_user",
|
||||
Discord = "test_user",
|
||||
Website = "https://google.com",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
89
osu.Game/Beatmaps/APIBeatmapMetadataSource.cs
Normal file
89
osu.Game/Beatmaps/APIBeatmapMetadataSource.cs
Normal file
@ -0,0 +1,89 @@
|
||||
// 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.Diagnostics;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs online metadata lookups using the osu-web API.
|
||||
/// </summary>
|
||||
public class APIBeatmapMetadataSource : IOnlineBeatmapMetadataSource
|
||||
{
|
||||
private readonly IAPIProvider api;
|
||||
|
||||
public APIBeatmapMetadataSource(IAPIProvider api)
|
||||
{
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
public bool Available => api.State.Value == APIState.Online;
|
||||
|
||||
public bool TryLookup(BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata)
|
||||
{
|
||||
if (!Available)
|
||||
{
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
var req = new GetBeatmapRequest(beatmapInfo);
|
||||
|
||||
try
|
||||
{
|
||||
// intentionally blocking to limit web request concurrency
|
||||
api.Perform(req);
|
||||
|
||||
if (req.CompletionState == APIRequestCompletionState.Failed)
|
||||
{
|
||||
logForModel(beatmapInfo.BeatmapSet, $@"Online retrieval failed for {beatmapInfo}");
|
||||
onlineMetadata = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
var res = req.Response;
|
||||
|
||||
if (res != null)
|
||||
{
|
||||
logForModel(beatmapInfo.BeatmapSet, $@"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineID}.");
|
||||
|
||||
onlineMetadata = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapID = res.OnlineID,
|
||||
BeatmapSetID = res.OnlineBeatmapSetID,
|
||||
AuthorID = res.AuthorID,
|
||||
BeatmapStatus = res.Status,
|
||||
BeatmapSetStatus = res.BeatmapSet?.Status,
|
||||
DateRanked = res.BeatmapSet?.Ranked,
|
||||
DateSubmitted = res.BeatmapSet?.Submitted,
|
||||
MD5Hash = res.MD5Hash,
|
||||
LastUpdated = res.LastUpdated
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logForModel(beatmapInfo.BeatmapSet, $@"Online retrieval failed for {beatmapInfo} ({e.Message})");
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void logForModel(BeatmapSetInfo set, string message) =>
|
||||
RealmArchiveModelImporter<BeatmapSetInfo>.LogForModel(set, $@"[{nameof(APIBeatmapMetadataSource)}] {message}");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
private TextureUpload limitTextureUploadSize(TextureUpload textureUpload)
|
||||
{
|
||||
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
|
||||
var image = Image.LoadPixelData(textureUpload.Data, textureUpload.Width, textureUpload.Height);
|
||||
|
||||
// The original texture upload will no longer be returned or used.
|
||||
textureUpload.Dispose();
|
||||
|
@ -1,62 +1,31 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using SharpCompress.Compressors;
|
||||
using SharpCompress.Compressors.BZip2;
|
||||
using SQLitePCL;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// A component which handles population of online IDs for beatmaps using a two part lookup procedure.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// On creating the component, a copy of a database containing metadata for a large subset of beatmaps (stored to <see cref="cache_database_name"/>) will be downloaded if not already present locally.
|
||||
/// This will always be checked before doing a second online query to get required metadata.
|
||||
/// </remarks>
|
||||
public class BeatmapUpdaterMetadataLookup : IDisposable
|
||||
{
|
||||
private readonly IAPIProvider api;
|
||||
private readonly Storage storage;
|
||||
|
||||
private FileWebRequest cacheDownloadRequest;
|
||||
|
||||
private const string cache_database_name = "online.db";
|
||||
private readonly IOnlineBeatmapMetadataSource apiMetadataSource;
|
||||
private readonly IOnlineBeatmapMetadataSource localCachedMetadataSource;
|
||||
|
||||
public BeatmapUpdaterMetadataLookup(IAPIProvider api, Storage storage)
|
||||
: this(new APIBeatmapMetadataSource(api), new LocalCachedBeatmapMetadataSource(storage))
|
||||
{
|
||||
try
|
||||
{
|
||||
// required to initialise native SQLite libraries on some platforms.
|
||||
Batteries_V2.Init();
|
||||
raw.sqlite3_config(2 /*SQLITE_CONFIG_MULTITHREAD*/);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// may fail if platform not supported.
|
||||
}
|
||||
}
|
||||
|
||||
this.api = api;
|
||||
this.storage = storage;
|
||||
|
||||
// avoid downloading / using cache for unit tests.
|
||||
if (!DebugUtils.IsNUnitRunning && !storage.Exists(cache_database_name))
|
||||
prepareLocalCache();
|
||||
internal BeatmapUpdaterMetadataLookup(IOnlineBeatmapMetadataSource apiMetadataSource, IOnlineBeatmapMetadataSource localCachedMetadataSource)
|
||||
{
|
||||
this.apiMetadataSource = apiMetadataSource;
|
||||
this.localCachedMetadataSource = localCachedMetadataSource;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -69,196 +38,72 @@ namespace osu.Game.Beatmaps
|
||||
/// <param name="preferOnlineFetch">Whether metadata from an online source should be preferred. If <c>true</c>, the local cache will be skipped to ensure the freshest data state possible.</param>
|
||||
public void Update(BeatmapSetInfo beatmapSet, bool preferOnlineFetch)
|
||||
{
|
||||
foreach (var b in beatmapSet.Beatmaps)
|
||||
lookup(beatmapSet, b, preferOnlineFetch);
|
||||
}
|
||||
|
||||
private void lookup(BeatmapSetInfo set, BeatmapInfo beatmapInfo, bool preferOnlineFetch)
|
||||
{
|
||||
bool apiAvailable = api?.State.Value == APIState.Online;
|
||||
|
||||
bool useLocalCache = !apiAvailable || !preferOnlineFetch;
|
||||
|
||||
if (useLocalCache && checkLocalCache(set, beatmapInfo))
|
||||
return;
|
||||
|
||||
if (!apiAvailable)
|
||||
return;
|
||||
|
||||
var req = new GetBeatmapRequest(beatmapInfo);
|
||||
|
||||
try
|
||||
foreach (var beatmapInfo in beatmapSet.Beatmaps)
|
||||
{
|
||||
// intentionally blocking to limit web request concurrency
|
||||
api.Perform(req);
|
||||
if (!tryLookup(beatmapInfo, preferOnlineFetch, out var res))
|
||||
continue;
|
||||
|
||||
if (req.CompletionState == APIRequestCompletionState.Failed)
|
||||
if (res == null)
|
||||
{
|
||||
logForModel(set, $"Online retrieval failed for {beatmapInfo}");
|
||||
beatmapInfo.ResetOnlineInfo();
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
var res = req.Response;
|
||||
beatmapInfo.OnlineID = res.BeatmapID;
|
||||
beatmapInfo.OnlineMD5Hash = res.MD5Hash;
|
||||
beatmapInfo.LastOnlineUpdate = res.LastUpdated;
|
||||
|
||||
if (res != null)
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
beatmapInfo.BeatmapSet.OnlineID = res.BeatmapSetID;
|
||||
|
||||
// Some metadata should only be applied if there's no local changes.
|
||||
if (shouldSaveOnlineMetadata(beatmapInfo))
|
||||
{
|
||||
beatmapInfo.OnlineID = res.OnlineID;
|
||||
beatmapInfo.OnlineMD5Hash = res.MD5Hash;
|
||||
beatmapInfo.LastOnlineUpdate = res.LastUpdated;
|
||||
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID;
|
||||
|
||||
// Some metadata should only be applied if there's no local changes.
|
||||
if (shouldSaveOnlineMetadata(beatmapInfo))
|
||||
{
|
||||
beatmapInfo.Status = res.Status;
|
||||
beatmapInfo.Metadata.Author.OnlineID = res.AuthorID;
|
||||
}
|
||||
|
||||
if (beatmapInfo.BeatmapSet.Beatmaps.All(shouldSaveOnlineMetadata))
|
||||
{
|
||||
beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapOnlineStatus.None;
|
||||
beatmapInfo.BeatmapSet.DateRanked = res.BeatmapSet?.Ranked;
|
||||
beatmapInfo.BeatmapSet.DateSubmitted = res.BeatmapSet?.Submitted;
|
||||
}
|
||||
|
||||
logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineID}.");
|
||||
beatmapInfo.Status = res.BeatmapStatus;
|
||||
beatmapInfo.Metadata.Author.OnlineID = res.AuthorID;
|
||||
}
|
||||
|
||||
if (beatmapInfo.BeatmapSet.Beatmaps.All(shouldSaveOnlineMetadata))
|
||||
{
|
||||
beatmapInfo.BeatmapSet.Status = res.BeatmapSetStatus ?? BeatmapOnlineStatus.None;
|
||||
beatmapInfo.BeatmapSet.DateRanked = res.DateRanked;
|
||||
beatmapInfo.BeatmapSet.DateSubmitted = res.DateSubmitted;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logForModel(set, $"Online retrieval failed for {beatmapInfo} ({e.Message})");
|
||||
beatmapInfo.ResetOnlineInfo();
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareLocalCache()
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the <see cref="OnlineBeatmapMetadata"/> for the given <paramref name="beatmapInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="beatmapInfo">The beatmap to perform the online lookup for.</param>
|
||||
/// <param name="preferOnlineFetch">Whether online sources should be preferred for the lookup.</param>
|
||||
/// <param name="result">The result of the lookup. Can be <see langword="null"/> if no matching beatmap was found (or the lookup failed).</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if any of the metadata sources were available and returned a valid <paramref name="result"/>.
|
||||
/// <see langword="false"/> if none of the metadata sources were available, or if there was insufficient data to return a valid <paramref name="result"/>.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// There are two cases wherein this method will return <see langword="false"/>:
|
||||
/// <list type="bullet">
|
||||
/// <item>If neither the local cache or the API are available to query.</item>
|
||||
/// <item>If the API is not available to query, and a positive match was not made in the local cache.</item>
|
||||
/// </list>
|
||||
/// In either case, the online ID read from the .osu file will be preserved, which may not necessarily be what we want.
|
||||
/// TODO: reconsider this if/when a better flow for queueing online retrieval is implemented.
|
||||
/// </remarks>
|
||||
private bool tryLookup(BeatmapInfo beatmapInfo, bool preferOnlineFetch, out OnlineBeatmapMetadata? result)
|
||||
{
|
||||
string cacheFilePath = storage.GetFullPath(cache_database_name);
|
||||
string compressedCacheFilePath = $"{cacheFilePath}.bz2";
|
||||
bool useLocalCache = !apiMetadataSource.Available || !preferOnlineFetch;
|
||||
if (useLocalCache && localCachedMetadataSource.TryLookup(beatmapInfo, out result))
|
||||
return true;
|
||||
|
||||
cacheDownloadRequest = new FileWebRequest(compressedCacheFilePath, $"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2?{DateTimeOffset.UtcNow:yyyyMMdd}");
|
||||
|
||||
cacheDownloadRequest.Failed += ex =>
|
||||
{
|
||||
File.Delete(compressedCacheFilePath);
|
||||
File.Delete(cacheFilePath);
|
||||
|
||||
Logger.Log($"{nameof(BeatmapUpdaterMetadataLookup)}'s online cache download failed: {ex}", LoggingTarget.Database);
|
||||
};
|
||||
|
||||
cacheDownloadRequest.Finished += () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var stream = File.OpenRead(cacheDownloadRequest.Filename))
|
||||
using (var outStream = File.OpenWrite(cacheFilePath))
|
||||
using (var bz2 = new BZip2Stream(stream, CompressionMode.Decompress, false))
|
||||
bz2.CopyTo(outStream);
|
||||
|
||||
// set to null on completion to allow lookups to begin using the new source
|
||||
cacheDownloadRequest = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"{nameof(BeatmapUpdaterMetadataLookup)}'s online cache extraction failed: {ex}", LoggingTarget.Database);
|
||||
File.Delete(cacheFilePath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(compressedCacheFilePath);
|
||||
}
|
||||
};
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await cacheDownloadRequest.PerformAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Prevent throwing unobserved exceptions, as they will be logged from the network request to the log file anyway.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmapInfo)
|
||||
{
|
||||
// download is in progress (or was, and failed).
|
||||
if (cacheDownloadRequest != null)
|
||||
return false;
|
||||
|
||||
// database is unavailable.
|
||||
if (!storage.Exists(cache_database_name))
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrEmpty(beatmapInfo.MD5Hash)
|
||||
&& string.IsNullOrEmpty(beatmapInfo.Path)
|
||||
&& beatmapInfo.OnlineID <= 0)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using (var db = new SqliteConnection(string.Concat("Data Source=", storage.GetFullPath($@"{"online.db"}", true))))
|
||||
{
|
||||
db.Open();
|
||||
|
||||
using (var cmd = db.CreateCommand())
|
||||
{
|
||||
cmd.CommandText =
|
||||
"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path";
|
||||
|
||||
cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmapInfo.MD5Hash));
|
||||
cmd.Parameters.Add(new SqliteParameter("@OnlineID", beatmapInfo.OnlineID));
|
||||
cmd.Parameters.Add(new SqliteParameter("@Path", beatmapInfo.Path));
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
{
|
||||
var status = (BeatmapOnlineStatus)reader.GetByte(2);
|
||||
|
||||
// Some metadata should only be applied if there's no local changes.
|
||||
if (shouldSaveOnlineMetadata(beatmapInfo))
|
||||
{
|
||||
beatmapInfo.Status = status;
|
||||
beatmapInfo.Metadata.Author.OnlineID = reader.GetInt32(3);
|
||||
}
|
||||
|
||||
// TODO: DateSubmitted and DateRanked are not provided by local cache.
|
||||
beatmapInfo.OnlineID = reader.GetInt32(1);
|
||||
beatmapInfo.OnlineMD5Hash = reader.GetString(4);
|
||||
beatmapInfo.LastOnlineUpdate = reader.GetDateTimeOffset(5);
|
||||
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0);
|
||||
|
||||
if (beatmapInfo.BeatmapSet.Beatmaps.All(shouldSaveOnlineMetadata))
|
||||
{
|
||||
beatmapInfo.BeatmapSet.Status = status;
|
||||
}
|
||||
|
||||
logForModel(set, $"Cached local retrieval for {beatmapInfo}.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logForModel(set, $"Cached local retrieval for {beatmapInfo} failed with {ex}.");
|
||||
}
|
||||
if (apiMetadataSource.TryLookup(beatmapInfo, out result))
|
||||
return true;
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void logForModel(BeatmapSetInfo set, string message) =>
|
||||
RealmArchiveModelImporter<BeatmapSetInfo>.LogForModel(set, $"[{nameof(BeatmapUpdaterMetadataLookup)}] {message}");
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the provided beatmap is in a state where online "ranked" status metadata should be saved against it.
|
||||
/// Handles the case where a user may have locally modified a beatmap in the editor and expects the local status to stick.
|
||||
@ -267,7 +112,8 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
cacheDownloadRequest?.Dispose();
|
||||
apiMetadataSource.Dispose();
|
||||
localCachedMetadataSource.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
32
osu.Game/Beatmaps/IOnlineBeatmapMetadataSource.cs
Normal file
32
osu.Game/Beatmaps/IOnlineBeatmapMetadataSource.cs
Normal file
@ -0,0 +1,32 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// Unifying interface for sources of <see cref="OnlineBeatmapMetadata"/>.
|
||||
/// </summary>
|
||||
public interface IOnlineBeatmapMetadataSource : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this source can currently service lookups.
|
||||
/// </summary>
|
||||
bool Available { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Looks up the online metadata for the supplied <paramref name="beatmapInfo"/>.
|
||||
/// </summary>
|
||||
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to look up.</param>
|
||||
/// <param name="onlineMetadata">
|
||||
/// An <see cref="OnlineBeatmapMetadata"/> instance if the lookup is successful.
|
||||
/// <see langword="null"/> if a mismatch between the local instance and the looked-up data was detected.
|
||||
/// The returned value is only valid if the return value of the method is <see langword="true"/>.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Whether the lookup was performed.
|
||||
/// </returns>
|
||||
bool TryLookup(BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata);
|
||||
}
|
||||
}
|
184
osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs
Normal file
184
osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs
Normal file
@ -0,0 +1,184 @@
|
||||
// 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.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using SharpCompress.Compressors;
|
||||
using SharpCompress.Compressors.BZip2;
|
||||
using SQLitePCL;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs online metadata lookups using a copy of a database containing metadata for a large subset of beatmaps (stored to <see cref="cache_database_name"/>).
|
||||
/// The database will be asynchronously downloaded - if not already present locally - when this component is constructed.
|
||||
/// </summary>
|
||||
public class LocalCachedBeatmapMetadataSource : IOnlineBeatmapMetadataSource
|
||||
{
|
||||
private readonly Storage storage;
|
||||
|
||||
private FileWebRequest? cacheDownloadRequest;
|
||||
|
||||
private const string cache_database_name = @"online.db";
|
||||
|
||||
public LocalCachedBeatmapMetadataSource(Storage storage)
|
||||
{
|
||||
try
|
||||
{
|
||||
// required to initialise native SQLite libraries on some platforms.
|
||||
Batteries_V2.Init();
|
||||
raw.sqlite3_config(2 /*SQLITE_CONFIG_MULTITHREAD*/);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// may fail if platform not supported.
|
||||
}
|
||||
|
||||
this.storage = storage;
|
||||
|
||||
// avoid downloading / using cache for unit tests.
|
||||
if (!DebugUtils.IsNUnitRunning && !storage.Exists(cache_database_name))
|
||||
prepareLocalCache();
|
||||
}
|
||||
|
||||
public bool Available =>
|
||||
// no download in progress.
|
||||
cacheDownloadRequest == null
|
||||
// cached database exists on disk.
|
||||
&& storage.Exists(cache_database_name);
|
||||
|
||||
public bool TryLookup(BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata)
|
||||
{
|
||||
if (!Available)
|
||||
{
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(beatmapInfo.MD5Hash)
|
||||
&& string.IsNullOrEmpty(beatmapInfo.Path)
|
||||
&& beatmapInfo.OnlineID <= 0)
|
||||
{
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
try
|
||||
{
|
||||
using (var db = new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true))))
|
||||
{
|
||||
db.Open();
|
||||
|
||||
using (var cmd = db.CreateCommand())
|
||||
{
|
||||
cmd.CommandText =
|
||||
@"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path";
|
||||
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash));
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@OnlineID", beatmapInfo.OnlineID));
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path));
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
{
|
||||
logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo}.");
|
||||
|
||||
onlineMetadata = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapSetID = reader.GetInt32(0),
|
||||
BeatmapID = reader.GetInt32(1),
|
||||
BeatmapStatus = (BeatmapOnlineStatus)reader.GetByte(2),
|
||||
BeatmapSetStatus = (BeatmapOnlineStatus)reader.GetByte(2),
|
||||
AuthorID = reader.GetInt32(3),
|
||||
MD5Hash = reader.GetString(4),
|
||||
LastUpdated = reader.GetDateTimeOffset(5),
|
||||
// TODO: DateSubmitted and DateRanked are not provided by local cache.
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} failed with {ex}.");
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void prepareLocalCache()
|
||||
{
|
||||
string cacheFilePath = storage.GetFullPath(cache_database_name);
|
||||
string compressedCacheFilePath = $@"{cacheFilePath}.bz2";
|
||||
|
||||
cacheDownloadRequest = new FileWebRequest(compressedCacheFilePath, $@"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2?{DateTimeOffset.UtcNow:yyyyMMdd}");
|
||||
|
||||
cacheDownloadRequest.Failed += ex =>
|
||||
{
|
||||
File.Delete(compressedCacheFilePath);
|
||||
File.Delete(cacheFilePath);
|
||||
|
||||
Logger.Log($@"{nameof(BeatmapUpdaterMetadataLookup)}'s online cache download failed: {ex}", LoggingTarget.Database);
|
||||
};
|
||||
|
||||
cacheDownloadRequest.Finished += () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var stream = File.OpenRead(cacheDownloadRequest.Filename))
|
||||
using (var outStream = File.OpenWrite(cacheFilePath))
|
||||
using (var bz2 = new BZip2Stream(stream, CompressionMode.Decompress, false))
|
||||
bz2.CopyTo(outStream);
|
||||
|
||||
// set to null on completion to allow lookups to begin using the new source
|
||||
cacheDownloadRequest = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($@"{nameof(LocalCachedBeatmapMetadataSource)}'s online cache extraction failed: {ex}", LoggingTarget.Database);
|
||||
File.Delete(cacheFilePath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(compressedCacheFilePath);
|
||||
}
|
||||
};
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await cacheDownloadRequest.PerformAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Prevent throwing unobserved exceptions, as they will be logged from the network request to the log file anyway.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void logForModel(BeatmapSetInfo set, string message) =>
|
||||
RealmArchiveModelImporter<BeatmapSetInfo>.LogForModel(set, $@"[{nameof(LocalCachedBeatmapMetadataSource)}] {message}");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
cacheDownloadRequest?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
61
osu.Game/Beatmaps/OnlineBeatmapMetadata.cs
Normal file
61
osu.Game/Beatmaps/OnlineBeatmapMetadata.cs
Normal file
@ -0,0 +1,61 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// This structure contains parts of beatmap metadata which are involved with the online parts
|
||||
/// of the game, and therefore must be treated with particular care.
|
||||
/// This data is retrieved from trusted sources (such as osu-web API, or a locally downloaded sqlite snapshot
|
||||
/// of osu-web metadata).
|
||||
/// </summary>
|
||||
public class OnlineBeatmapMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// The online ID of the beatmap.
|
||||
/// </summary>
|
||||
public int BeatmapID { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The online ID of the beatmap set.
|
||||
/// </summary>
|
||||
public int BeatmapSetID { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The online ID of the author.
|
||||
/// </summary>
|
||||
public int AuthorID { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The online status of the beatmap.
|
||||
/// </summary>
|
||||
public BeatmapOnlineStatus BeatmapStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The online status of the associated beatmap set.
|
||||
/// </summary>
|
||||
public BeatmapOnlineStatus? BeatmapSetStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The rank date of the beatmap, if applicable and available.
|
||||
/// </summary>
|
||||
public DateTimeOffset? DateRanked { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The submission date of the beatmap, if available.
|
||||
/// </summary>
|
||||
public DateTimeOffset? DateSubmitted { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The MD5 hash of the beatmap. Used to verify integrity.
|
||||
/// </summary>
|
||||
public string MD5Hash { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The date when this metadata was last updated.
|
||||
/// </summary>
|
||||
public DateTimeOffset LastUpdated { get; init; }
|
||||
}
|
||||
}
|
@ -23,9 +23,12 @@ namespace osu.Game.Online.Multiplayer
|
||||
|
||||
Debug.Assert(exception != null);
|
||||
|
||||
string message = exception.GetHubExceptionMessage() ?? exception.Message;
|
||||
if (exception.GetHubExceptionMessage() is string message)
|
||||
// Hub exceptions generally contain something we can show the user directly.
|
||||
Logger.Log(message, level: LogLevel.Important);
|
||||
else
|
||||
Logger.Error(exception, $"Unobserved exception occurred via {nameof(FireAndForget)} call: {exception.Message}");
|
||||
|
||||
Logger.Log(message, level: LogLevel.Important);
|
||||
onError?.Invoke(exception);
|
||||
}
|
||||
else
|
||||
|
@ -1,8 +1,10 @@
|
||||
// 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.Globalization;
|
||||
using System.Net.Http;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
|
||||
namespace osu.Game.Online.Rooms
|
||||
@ -11,12 +13,16 @@ namespace osu.Game.Online.Rooms
|
||||
{
|
||||
private readonly long roomId;
|
||||
private readonly long playlistItemId;
|
||||
private readonly BeatmapInfo beatmapInfo;
|
||||
private readonly int rulesetId;
|
||||
private readonly string versionHash;
|
||||
|
||||
public CreateRoomScoreRequest(long roomId, long playlistItemId, string versionHash)
|
||||
public CreateRoomScoreRequest(long roomId, long playlistItemId, BeatmapInfo beatmapInfo, int rulesetId, string versionHash)
|
||||
{
|
||||
this.roomId = roomId;
|
||||
this.playlistItemId = playlistItemId;
|
||||
this.beatmapInfo = beatmapInfo;
|
||||
this.rulesetId = rulesetId;
|
||||
this.versionHash = versionHash;
|
||||
}
|
||||
|
||||
@ -25,6 +31,8 @@ namespace osu.Game.Online.Rooms
|
||||
var req = base.CreateWebRequest();
|
||||
req.Method = HttpMethod.Post;
|
||||
req.AddParameter("version_hash", versionHash);
|
||||
req.AddParameter("beatmap_hash", beatmapInfo.MD5Hash);
|
||||
req.AddParameter("ruleset_id", rulesetId.ToString(CultureInfo.InvariantCulture));
|
||||
return req;
|
||||
}
|
||||
|
||||
|
@ -144,8 +144,8 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
|
||||
bool anyInfoAdded = false;
|
||||
|
||||
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.MapMarker, user.Location);
|
||||
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Heart, user.Interests);
|
||||
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.MapMarkerAlt, user.Location);
|
||||
anyInfoAdded |= tryAddInfo(FontAwesome.Regular.Heart, user.Interests);
|
||||
anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Suitcase, user.Occupation);
|
||||
|
||||
if (anyInfoAdded)
|
||||
@ -171,7 +171,7 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
|
||||
bottomLinkContainer.AddIcon(icon, text =>
|
||||
{
|
||||
text.Font = text.Font.With(size: 10);
|
||||
text.Font = text.Font.With(icon.Family, 10, icon.Weight);
|
||||
text.Colour = iconColour;
|
||||
});
|
||||
|
||||
|
@ -4,7 +4,6 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Difficulty.Preprocessing
|
||||
@ -65,8 +64,16 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing
|
||||
EndTime = hitObject.GetEndTime() / clockRate;
|
||||
}
|
||||
|
||||
public DifficultyHitObject Previous(int backwardsIndex) => difficultyHitObjects.ElementAtOrDefault(Index - (backwardsIndex + 1));
|
||||
public DifficultyHitObject Previous(int backwardsIndex)
|
||||
{
|
||||
int index = Index - (backwardsIndex + 1);
|
||||
return index >= 0 && index < difficultyHitObjects.Count ? difficultyHitObjects[index] : default;
|
||||
}
|
||||
|
||||
public DifficultyHitObject Next(int forwardsIndex) => difficultyHitObjects.ElementAtOrDefault(Index + (forwardsIndex + 1));
|
||||
public DifficultyHitObject Next(int forwardsIndex)
|
||||
{
|
||||
int index = Index + (forwardsIndex + 1);
|
||||
return index >= 0 && index < difficultyHitObjects.Count ? difficultyHitObjects[index] : default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Diagnostics;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Scoring;
|
||||
@ -30,7 +31,16 @@ namespace osu.Game.Screens.Play
|
||||
if (!(Room.RoomID.Value is long roomId))
|
||||
return null;
|
||||
|
||||
return new CreateRoomScoreRequest(roomId, PlaylistItem.ID, Game.VersionHash);
|
||||
int beatmapId = Beatmap.Value.BeatmapInfo.OnlineID;
|
||||
int rulesetId = Ruleset.Value.OnlineID;
|
||||
|
||||
if (beatmapId <= 0)
|
||||
return null;
|
||||
|
||||
if (!Ruleset.Value.IsLegacyRuleset())
|
||||
return null;
|
||||
|
||||
return new CreateRoomScoreRequest(roomId, PlaylistItem.ID, Beatmap.Value.BeatmapInfo, rulesetId, Game.VersionHash);
|
||||
}
|
||||
|
||||
protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token)
|
||||
|
@ -1,9 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
@ -22,10 +21,10 @@ namespace osu.Game.Updater
|
||||
/// </summary>
|
||||
public partial class SimpleUpdateManager : UpdateManager
|
||||
{
|
||||
private string version;
|
||||
private string version = null!;
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
private GameHost host { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuGameBase game)
|
||||
@ -48,7 +47,7 @@ namespace osu.Game.Updater
|
||||
version = version.Split('-').First();
|
||||
string latestTagName = latest.TagName.Split('-').First();
|
||||
|
||||
if (latestTagName != version)
|
||||
if (latestTagName != version && tryGetBestUrl(latest, out string? url))
|
||||
{
|
||||
Notifications.Post(new SimpleNotification
|
||||
{
|
||||
@ -57,7 +56,7 @@ namespace osu.Game.Updater
|
||||
Icon = FontAwesome.Solid.Download,
|
||||
Activated = () =>
|
||||
{
|
||||
host.OpenUrlExternally(getBestUrl(latest));
|
||||
host.OpenUrlExternally(url);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@ -74,9 +73,10 @@ namespace osu.Game.Updater
|
||||
return false;
|
||||
}
|
||||
|
||||
private string getBestUrl(GitHubRelease release)
|
||||
private bool tryGetBestUrl(GitHubRelease release, [NotNullWhen(true)] out string? url)
|
||||
{
|
||||
GitHubAsset bestAsset = null;
|
||||
url = null;
|
||||
GitHubAsset? bestAsset = null;
|
||||
|
||||
switch (RuntimeInfo.OS)
|
||||
{
|
||||
@ -94,17 +94,23 @@ namespace osu.Game.Updater
|
||||
break;
|
||||
|
||||
case RuntimeInfo.Platform.iOS:
|
||||
// iOS releases are available via testflight. this link seems to work well enough for now.
|
||||
// see https://stackoverflow.com/a/32960501
|
||||
return "itms-beta://beta.itunes.apple.com/v1/app/1447765923";
|
||||
if (release.Assets?.Exists(f => f.Name.EndsWith(".ipa", StringComparison.Ordinal)) == true)
|
||||
// iOS releases are available via testflight. this link seems to work well enough for now.
|
||||
// see https://stackoverflow.com/a/32960501
|
||||
url = "itms-beta://beta.itunes.apple.com/v1/app/1447765923";
|
||||
|
||||
break;
|
||||
|
||||
case RuntimeInfo.Platform.Android:
|
||||
// on our testing device this causes the download to magically disappear.
|
||||
//bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".apk"));
|
||||
if (release.Assets?.Exists(f => f.Name.EndsWith(".apk", StringComparison.Ordinal)) == true)
|
||||
// on our testing device using the .apk URL causes the download to magically disappear.
|
||||
url = release.HtmlUrl;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return bestAsset?.BrowserDownloadUrl ?? release.HtmlUrl;
|
||||
url ??= bestAsset?.BrowserDownloadUrl;
|
||||
return url != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user