mirror of
https://github.com/osukey/osukey.git
synced 2025-06-29 07:07:55 +09:00
Merge branch 'master' into fix-password-popover-focus
This commit is contained in:
commit
c6ca0e5895
@ -28,7 +28,12 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
|||||||
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
|
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
|
||||||
{
|
{
|
||||||
new TrackedSetting<double>(ManiaRulesetSetting.ScrollTime,
|
new TrackedSetting<double>(ManiaRulesetSetting.ScrollTime,
|
||||||
v => new SettingDescription(v, "Scroll Speed", $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / v)} ({v}ms)"))
|
scrollTime => new SettingDescription(
|
||||||
|
rawValue: scrollTime,
|
||||||
|
name: "Scroll Speed",
|
||||||
|
value: $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)} ({scrollTime}ms)"
|
||||||
|
)
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
820
osu.Game.Tests/Database/BeatmapImporterTests.cs
Normal file
820
osu.Game.Tests/Database/BeatmapImporterTests.cs
Normal file
@ -0,0 +1,820 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.IO.Archives;
|
||||||
|
using osu.Game.Models;
|
||||||
|
using osu.Game.Stores;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
using Realms;
|
||||||
|
using SharpCompress.Archives;
|
||||||
|
using SharpCompress.Archives.Zip;
|
||||||
|
using SharpCompress.Common;
|
||||||
|
using SharpCompress.Writers.Zip;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Database
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class BeatmapImporterTests : RealmTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestImportBeatmapThenCleanup()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using (var importer = new BeatmapImporter(realmFactory, storage))
|
||||||
|
using (new RealmRulesetStore(realmFactory, storage))
|
||||||
|
{
|
||||||
|
ILive<RealmBeatmapSet>? imported;
|
||||||
|
|
||||||
|
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
|
||||||
|
imported = await importer.Import(reader);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, realmFactory.Context.All<RealmBeatmapSet>().Count());
|
||||||
|
|
||||||
|
Assert.NotNull(imported);
|
||||||
|
Debug.Assert(imported != null);
|
||||||
|
|
||||||
|
imported.PerformWrite(s => s.DeletePending = true);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, realmFactory.Context.All<RealmBeatmapSet>().Count(s => s.DeletePending));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Logger.Log("Running with no work to purge pending deletions");
|
||||||
|
|
||||||
|
RunTestWithRealm((realmFactory, _) => { Assert.AreEqual(0, realmFactory.Context.All<RealmBeatmapSet>().Count()); });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportWhenClosed()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
await LoadOszIntoStore(importer, realmFactory.Context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportThenDelete()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||||
|
|
||||||
|
deleteBeatmapSet(imported, realmFactory.Context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportThenDeleteFromStream()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var tempPath = TestResources.GetTestBeatmapForImport();
|
||||||
|
|
||||||
|
ILive<RealmBeatmapSet>? importedSet;
|
||||||
|
|
||||||
|
using (var stream = File.OpenRead(tempPath))
|
||||||
|
{
|
||||||
|
importedSet = await importer.Import(new ImportTask(stream, Path.GetFileName(tempPath)));
|
||||||
|
ensureLoaded(realmFactory.Context);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.NotNull(importedSet);
|
||||||
|
Debug.Assert(importedSet != null);
|
||||||
|
|
||||||
|
Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing");
|
||||||
|
File.Delete(tempPath);
|
||||||
|
|
||||||
|
var imported = realmFactory.Context.All<RealmBeatmapSet>().First(beatmapSet => beatmapSet.ID == importedSet.ID);
|
||||||
|
|
||||||
|
deleteBeatmapSet(imported, realmFactory.Context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportThenImport()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||||
|
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||||
|
|
||||||
|
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
||||||
|
Assert.IsTrue(imported.ID == importedSecondTime.ID);
|
||||||
|
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
|
||||||
|
|
||||||
|
checkBeatmapSetCount(realmFactory.Context, 1);
|
||||||
|
checkSingleReferencedFileCount(realmFactory.Context, 18);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportThenImportWithReZip()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var temp = TestResources.GetTestBeatmapForImport();
|
||||||
|
|
||||||
|
string extractedFolder = $"{temp}_extracted";
|
||||||
|
Directory.CreateDirectory(extractedFolder);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||||
|
|
||||||
|
string hashBefore = hashFile(temp);
|
||||||
|
|
||||||
|
using (var zip = ZipArchive.Open(temp))
|
||||||
|
zip.WriteToDirectory(extractedFolder);
|
||||||
|
|
||||||
|
using (var zip = ZipArchive.Create())
|
||||||
|
{
|
||||||
|
zip.AddAllFromDirectory(extractedFolder);
|
||||||
|
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
|
||||||
|
}
|
||||||
|
|
||||||
|
// zip files differ because different compression or encoder.
|
||||||
|
Assert.AreNotEqual(hashBefore, hashFile(temp));
|
||||||
|
|
||||||
|
var importedSecondTime = await importer.Import(new ImportTask(temp));
|
||||||
|
|
||||||
|
ensureLoaded(realmFactory.Context);
|
||||||
|
|
||||||
|
Assert.NotNull(importedSecondTime);
|
||||||
|
Debug.Assert(importedSecondTime != null);
|
||||||
|
|
||||||
|
// but contents doesn't, so existing should still be used.
|
||||||
|
Assert.IsTrue(imported.ID == importedSecondTime.ID);
|
||||||
|
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.PerformRead(s => s.Beatmaps.First().ID));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(extractedFolder, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportThenImportWithChangedHashedFile()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var temp = TestResources.GetTestBeatmapForImport();
|
||||||
|
|
||||||
|
string extractedFolder = $"{temp}_extracted";
|
||||||
|
Directory.CreateDirectory(extractedFolder);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||||
|
|
||||||
|
await createScoreForBeatmap(realmFactory.Context, imported.Beatmaps.First());
|
||||||
|
|
||||||
|
using (var zip = ZipArchive.Open(temp))
|
||||||
|
zip.WriteToDirectory(extractedFolder);
|
||||||
|
|
||||||
|
// arbitrary write to hashed file
|
||||||
|
// this triggers the special BeatmapManager.PreImport deletion/replacement flow.
|
||||||
|
using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.osu").First()).AppendText())
|
||||||
|
await sw.WriteLineAsync("// changed");
|
||||||
|
|
||||||
|
using (var zip = ZipArchive.Create())
|
||||||
|
{
|
||||||
|
zip.AddAllFromDirectory(extractedFolder);
|
||||||
|
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
|
||||||
|
}
|
||||||
|
|
||||||
|
var importedSecondTime = await importer.Import(new ImportTask(temp));
|
||||||
|
|
||||||
|
ensureLoaded(realmFactory.Context);
|
||||||
|
|
||||||
|
// check the newly "imported" beatmap is not the original.
|
||||||
|
Assert.NotNull(importedSecondTime);
|
||||||
|
Debug.Assert(importedSecondTime != null);
|
||||||
|
|
||||||
|
Assert.IsTrue(imported.ID != importedSecondTime.ID);
|
||||||
|
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.PerformRead(s => s.Beatmaps.First().ID));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(extractedFolder, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Ignore("intentionally broken by import optimisations")]
|
||||||
|
public void TestImportThenImportWithChangedFile()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var temp = TestResources.GetTestBeatmapForImport();
|
||||||
|
|
||||||
|
string extractedFolder = $"{temp}_extracted";
|
||||||
|
Directory.CreateDirectory(extractedFolder);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||||
|
|
||||||
|
using (var zip = ZipArchive.Open(temp))
|
||||||
|
zip.WriteToDirectory(extractedFolder);
|
||||||
|
|
||||||
|
// arbitrary write to non-hashed file
|
||||||
|
using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.mp3").First()).AppendText())
|
||||||
|
await sw.WriteLineAsync("text");
|
||||||
|
|
||||||
|
using (var zip = ZipArchive.Create())
|
||||||
|
{
|
||||||
|
zip.AddAllFromDirectory(extractedFolder);
|
||||||
|
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
|
||||||
|
}
|
||||||
|
|
||||||
|
var importedSecondTime = await importer.Import(new ImportTask(temp));
|
||||||
|
|
||||||
|
ensureLoaded(realmFactory.Context);
|
||||||
|
|
||||||
|
Assert.NotNull(importedSecondTime);
|
||||||
|
Debug.Assert(importedSecondTime != null);
|
||||||
|
|
||||||
|
// check the newly "imported" beatmap is not the original.
|
||||||
|
Assert.IsTrue(imported.ID != importedSecondTime.ID);
|
||||||
|
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.PerformRead(s => s.Beatmaps.First().ID));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(extractedFolder, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportThenImportWithDifferentFilename()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var temp = TestResources.GetTestBeatmapForImport();
|
||||||
|
|
||||||
|
string extractedFolder = $"{temp}_extracted";
|
||||||
|
Directory.CreateDirectory(extractedFolder);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||||
|
|
||||||
|
using (var zip = ZipArchive.Open(temp))
|
||||||
|
zip.WriteToDirectory(extractedFolder);
|
||||||
|
|
||||||
|
// change filename
|
||||||
|
var firstFile = new FileInfo(Directory.GetFiles(extractedFolder).First());
|
||||||
|
firstFile.MoveTo(Path.Combine(firstFile.DirectoryName.AsNonNull(), $"{firstFile.Name}-changed{firstFile.Extension}"));
|
||||||
|
|
||||||
|
using (var zip = ZipArchive.Create())
|
||||||
|
{
|
||||||
|
zip.AddAllFromDirectory(extractedFolder);
|
||||||
|
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
|
||||||
|
}
|
||||||
|
|
||||||
|
var importedSecondTime = await importer.Import(new ImportTask(temp));
|
||||||
|
|
||||||
|
ensureLoaded(realmFactory.Context);
|
||||||
|
|
||||||
|
Assert.NotNull(importedSecondTime);
|
||||||
|
Debug.Assert(importedSecondTime != null);
|
||||||
|
|
||||||
|
// check the newly "imported" beatmap is not the original.
|
||||||
|
Assert.IsTrue(imported.ID != importedSecondTime.ID);
|
||||||
|
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.PerformRead(s => s.Beatmaps.First().ID));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(extractedFolder, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Ignore("intentionally broken by import optimisations")]
|
||||||
|
public void TestImportCorruptThenImport()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||||
|
|
||||||
|
var firstFile = imported.Files.First();
|
||||||
|
|
||||||
|
long originalLength;
|
||||||
|
using (var stream = storage.GetStream(firstFile.File.StoragePath))
|
||||||
|
originalLength = stream.Length;
|
||||||
|
|
||||||
|
using (var stream = storage.GetStream(firstFile.File.StoragePath, FileAccess.Write, FileMode.Create))
|
||||||
|
stream.WriteByte(0);
|
||||||
|
|
||||||
|
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||||
|
|
||||||
|
using (var stream = storage.GetStream(firstFile.File.StoragePath))
|
||||||
|
Assert.AreEqual(stream.Length, originalLength, "Corruption was not fixed on second import");
|
||||||
|
|
||||||
|
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
||||||
|
Assert.IsTrue(imported.ID == importedSecondTime.ID);
|
||||||
|
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
|
||||||
|
|
||||||
|
checkBeatmapSetCount(realmFactory.Context, 1);
|
||||||
|
checkSingleReferencedFileCount(realmFactory.Context, 18);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRollbackOnFailure()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
int loggedExceptionCount = 0;
|
||||||
|
|
||||||
|
Logger.NewEntry += l =>
|
||||||
|
{
|
||||||
|
if (l.Target == LoggingTarget.Database && l.Exception != null)
|
||||||
|
Interlocked.Increment(ref loggedExceptionCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||||
|
|
||||||
|
realmFactory.Context.Write(() => imported.Hash += "-changed");
|
||||||
|
|
||||||
|
checkBeatmapSetCount(realmFactory.Context, 1);
|
||||||
|
checkBeatmapCount(realmFactory.Context, 12);
|
||||||
|
checkSingleReferencedFileCount(realmFactory.Context, 18);
|
||||||
|
|
||||||
|
var brokenTempFilename = TestResources.GetTestBeatmapForImport();
|
||||||
|
|
||||||
|
MemoryStream brokenOsu = new MemoryStream();
|
||||||
|
MemoryStream brokenOsz = new MemoryStream(await File.ReadAllBytesAsync(brokenTempFilename));
|
||||||
|
|
||||||
|
File.Delete(brokenTempFilename);
|
||||||
|
|
||||||
|
using (var outStream = File.Open(brokenTempFilename, FileMode.CreateNew))
|
||||||
|
using (var zip = ZipArchive.Open(brokenOsz))
|
||||||
|
{
|
||||||
|
zip.AddEntry("broken.osu", brokenOsu, false);
|
||||||
|
zip.SaveTo(outStream, CompressionType.Deflate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await importer.Import(new ImportTask(brokenTempFilename));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
checkBeatmapSetCount(realmFactory.Context, 1);
|
||||||
|
checkBeatmapCount(realmFactory.Context, 12);
|
||||||
|
|
||||||
|
checkSingleReferencedFileCount(realmFactory.Context, 18);
|
||||||
|
|
||||||
|
Assert.AreEqual(1, loggedExceptionCount);
|
||||||
|
|
||||||
|
File.Delete(brokenTempFilename);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportThenDeleteThenImport()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||||
|
|
||||||
|
deleteBeatmapSet(imported, realmFactory.Context);
|
||||||
|
|
||||||
|
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||||
|
|
||||||
|
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
||||||
|
Assert.IsTrue(imported.ID == importedSecondTime.ID);
|
||||||
|
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportThenDeleteThenImportWithOnlineIDsMissing()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||||
|
|
||||||
|
realmFactory.Context.Write(() =>
|
||||||
|
{
|
||||||
|
foreach (var b in imported.Beatmaps)
|
||||||
|
b.OnlineID = -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteBeatmapSet(imported, realmFactory.Context);
|
||||||
|
|
||||||
|
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||||
|
|
||||||
|
// check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched)
|
||||||
|
Assert.IsTrue(imported.ID != importedSecondTime.ID);
|
||||||
|
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportWithDuplicateBeatmapIDs()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var metadata = new RealmBeatmapMetadata
|
||||||
|
{
|
||||||
|
Artist = "SomeArtist",
|
||||||
|
Author = "SomeAuthor"
|
||||||
|
};
|
||||||
|
|
||||||
|
var ruleset = realmFactory.Context.All<RealmRuleset>().First();
|
||||||
|
|
||||||
|
var toImport = new RealmBeatmapSet
|
||||||
|
{
|
||||||
|
OnlineID = 1,
|
||||||
|
Beatmaps =
|
||||||
|
{
|
||||||
|
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata)
|
||||||
|
{
|
||||||
|
OnlineID = 2,
|
||||||
|
},
|
||||||
|
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata)
|
||||||
|
{
|
||||||
|
OnlineID = 2,
|
||||||
|
Status = BeatmapSetOnlineStatus.Loved,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var imported = await importer.Import(toImport);
|
||||||
|
|
||||||
|
Assert.NotNull(imported);
|
||||||
|
Debug.Assert(imported != null);
|
||||||
|
|
||||||
|
Assert.AreEqual(-1, imported.PerformRead(s => s.Beatmaps[0].OnlineID));
|
||||||
|
Assert.AreEqual(-1, imported.PerformRead(s => s.Beatmaps[1].OnlineID));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportWhenFileOpen()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var temp = TestResources.GetTestBeatmapForImport();
|
||||||
|
using (File.OpenRead(temp))
|
||||||
|
await importer.Import(temp);
|
||||||
|
ensureLoaded(realmFactory.Context);
|
||||||
|
File.Delete(temp);
|
||||||
|
Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportWithDuplicateHashes()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var temp = TestResources.GetTestBeatmapForImport();
|
||||||
|
|
||||||
|
string extractedFolder = $"{temp}_extracted";
|
||||||
|
Directory.CreateDirectory(extractedFolder);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var zip = ZipArchive.Open(temp))
|
||||||
|
zip.WriteToDirectory(extractedFolder);
|
||||||
|
|
||||||
|
using (var zip = ZipArchive.Create())
|
||||||
|
{
|
||||||
|
zip.AddAllFromDirectory(extractedFolder);
|
||||||
|
zip.AddEntry("duplicate.osu", Directory.GetFiles(extractedFolder, "*.osu").First());
|
||||||
|
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
|
||||||
|
}
|
||||||
|
|
||||||
|
await importer.Import(temp);
|
||||||
|
|
||||||
|
ensureLoaded(realmFactory.Context);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(extractedFolder, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportNestedStructure()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var temp = TestResources.GetTestBeatmapForImport();
|
||||||
|
|
||||||
|
string extractedFolder = $"{temp}_extracted";
|
||||||
|
string subfolder = Path.Combine(extractedFolder, "subfolder");
|
||||||
|
|
||||||
|
Directory.CreateDirectory(subfolder);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var zip = ZipArchive.Open(temp))
|
||||||
|
zip.WriteToDirectory(subfolder);
|
||||||
|
|
||||||
|
using (var zip = ZipArchive.Create())
|
||||||
|
{
|
||||||
|
zip.AddAllFromDirectory(extractedFolder);
|
||||||
|
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
|
||||||
|
}
|
||||||
|
|
||||||
|
var imported = await importer.Import(new ImportTask(temp));
|
||||||
|
|
||||||
|
Assert.NotNull(imported);
|
||||||
|
Debug.Assert(imported != null);
|
||||||
|
|
||||||
|
ensureLoaded(realmFactory.Context);
|
||||||
|
|
||||||
|
Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("subfolder"))), "Files contain common subfolder");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(extractedFolder, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestImportWithIgnoredDirectoryInArchive()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var temp = TestResources.GetTestBeatmapForImport();
|
||||||
|
|
||||||
|
string extractedFolder = $"{temp}_extracted";
|
||||||
|
string dataFolder = Path.Combine(extractedFolder, "actual_data");
|
||||||
|
string resourceForkFolder = Path.Combine(extractedFolder, "__MACOSX");
|
||||||
|
string resourceForkFilePath = Path.Combine(resourceForkFolder, ".extracted");
|
||||||
|
|
||||||
|
Directory.CreateDirectory(dataFolder);
|
||||||
|
Directory.CreateDirectory(resourceForkFolder);
|
||||||
|
|
||||||
|
using (var resourceForkFile = File.CreateText(resourceForkFilePath))
|
||||||
|
{
|
||||||
|
await resourceForkFile.WriteLineAsync("adding content so that it's not empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var zip = ZipArchive.Open(temp))
|
||||||
|
zip.WriteToDirectory(dataFolder);
|
||||||
|
|
||||||
|
using (var zip = ZipArchive.Create())
|
||||||
|
{
|
||||||
|
zip.AddAllFromDirectory(extractedFolder);
|
||||||
|
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
|
||||||
|
}
|
||||||
|
|
||||||
|
var imported = await importer.Import(new ImportTask(temp));
|
||||||
|
|
||||||
|
Assert.NotNull(imported);
|
||||||
|
Debug.Assert(imported != null);
|
||||||
|
|
||||||
|
ensureLoaded(realmFactory.Context);
|
||||||
|
|
||||||
|
Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("__MACOSX"))), "Files contain resource fork folder, which should be ignored");
|
||||||
|
Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("actual_data"))), "Files contain common subfolder");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(extractedFolder, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUpdateBeatmapInfo()
|
||||||
|
{
|
||||||
|
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||||
|
{
|
||||||
|
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||||
|
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||||
|
|
||||||
|
var temp = TestResources.GetTestBeatmapForImport();
|
||||||
|
await importer.Import(temp);
|
||||||
|
|
||||||
|
// Update via the beatmap, not the beatmap info, to ensure correct linking
|
||||||
|
RealmBeatmapSet setToUpdate = realmFactory.Context.All<RealmBeatmapSet>().First();
|
||||||
|
|
||||||
|
var beatmapToUpdate = setToUpdate.Beatmaps.First();
|
||||||
|
|
||||||
|
realmFactory.Context.Write(() => beatmapToUpdate.DifficultyName = "updated");
|
||||||
|
|
||||||
|
RealmBeatmap updatedInfo = realmFactory.Context.All<RealmBeatmap>().First(b => b.ID == beatmapToUpdate.ID);
|
||||||
|
Assert.That(updatedInfo.DifficultyName, Is.EqualTo("updated"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<RealmBeatmapSet?> LoadQuickOszIntoOsu(BeatmapImporter importer, Realm realm)
|
||||||
|
{
|
||||||
|
var temp = TestResources.GetQuickTestBeatmapForImport();
|
||||||
|
|
||||||
|
var importedSet = await importer.Import(new ImportTask(temp));
|
||||||
|
|
||||||
|
Assert.NotNull(importedSet);
|
||||||
|
|
||||||
|
ensureLoaded(realm);
|
||||||
|
|
||||||
|
waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
|
||||||
|
|
||||||
|
return realm.All<RealmBeatmapSet>().FirstOrDefault(beatmapSet => beatmapSet.ID == importedSet!.ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<RealmBeatmapSet> LoadOszIntoStore(BeatmapImporter importer, Realm realm, string? path = null, bool virtualTrack = false)
|
||||||
|
{
|
||||||
|
var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);
|
||||||
|
|
||||||
|
var importedSet = await importer.Import(new ImportTask(temp));
|
||||||
|
|
||||||
|
Assert.NotNull(importedSet);
|
||||||
|
Debug.Assert(importedSet != null);
|
||||||
|
|
||||||
|
ensureLoaded(realm);
|
||||||
|
|
||||||
|
waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
|
||||||
|
|
||||||
|
return realm.All<RealmBeatmapSet>().First(beatmapSet => beatmapSet.ID == importedSet.ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteBeatmapSet(RealmBeatmapSet imported, Realm realm)
|
||||||
|
{
|
||||||
|
realm.Write(() => imported.DeletePending = true);
|
||||||
|
|
||||||
|
checkBeatmapSetCount(realm, 0);
|
||||||
|
checkBeatmapSetCount(realm, 1, true);
|
||||||
|
|
||||||
|
Assert.IsTrue(realm.All<RealmBeatmapSet>().First(_ => true).DeletePending);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task createScoreForBeatmap(Realm realm, RealmBeatmap beatmap)
|
||||||
|
{
|
||||||
|
// TODO: reimplement when we have score support in realm.
|
||||||
|
// return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
|
||||||
|
// {
|
||||||
|
// OnlineScoreID = 2,
|
||||||
|
// Beatmap = beatmap,
|
||||||
|
// BeatmapInfoID = beatmap.ID
|
||||||
|
// }, new ImportScoreTest.TestArchiveReader());
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkBeatmapSetCount(Realm realm, int expected, bool includeDeletePending = false)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(expected, includeDeletePending
|
||||||
|
? realm.All<RealmBeatmapSet>().Count()
|
||||||
|
: realm.All<RealmBeatmapSet>().Count(s => !s.DeletePending));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string hashFile(string filename)
|
||||||
|
{
|
||||||
|
using (var s = File.OpenRead(filename))
|
||||||
|
return s.ComputeMD5Hash();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkBeatmapCount(Realm realm, int expected)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(expected, realm.All<RealmBeatmap>().Where(_ => true).ToList().Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkSingleReferencedFileCount(Realm realm, int expected)
|
||||||
|
{
|
||||||
|
int singleReferencedCount = 0;
|
||||||
|
|
||||||
|
foreach (var f in realm.All<RealmFile>())
|
||||||
|
{
|
||||||
|
if (f.BacklinksCount == 1)
|
||||||
|
singleReferencedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.AreEqual(expected, singleReferencedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureLoaded(Realm realm, int timeout = 60000)
|
||||||
|
{
|
||||||
|
IQueryable<RealmBeatmapSet>? resultSets = null;
|
||||||
|
|
||||||
|
waitForOrAssert(() => (resultSets = realm.All<RealmBeatmapSet>().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any(),
|
||||||
|
@"BeatmapSet did not import to the database in allocated time.", timeout);
|
||||||
|
|
||||||
|
// ensure we were stored to beatmap database backing...
|
||||||
|
Assert.IsTrue(resultSets?.Count() == 1, $@"Incorrect result count found ({resultSets?.Count()} but should be 1).");
|
||||||
|
|
||||||
|
IEnumerable<RealmBeatmapSet> queryBeatmapSets() => realm.All<RealmBeatmapSet>().Where(s => !s.DeletePending && s.OnlineID == 241526);
|
||||||
|
|
||||||
|
var set = queryBeatmapSets().First();
|
||||||
|
|
||||||
|
// ReSharper disable once PossibleUnintendedReferenceComparison
|
||||||
|
IEnumerable<RealmBeatmap> queryBeatmaps() => realm.All<RealmBeatmap>().Where(s => s.BeatmapSet != null && s.BeatmapSet == set);
|
||||||
|
|
||||||
|
waitForOrAssert(() => queryBeatmaps().Count() == 12, @"Beatmaps did not import to the database in allocated time", timeout);
|
||||||
|
waitForOrAssert(() => queryBeatmapSets().Count() == 1, @"BeatmapSet did not import to the database in allocated time", timeout);
|
||||||
|
|
||||||
|
int countBeatmapSetBeatmaps = 0;
|
||||||
|
int countBeatmaps = 0;
|
||||||
|
|
||||||
|
waitForOrAssert(() =>
|
||||||
|
(countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) ==
|
||||||
|
(countBeatmaps = queryBeatmaps().Count()),
|
||||||
|
$@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).", timeout);
|
||||||
|
|
||||||
|
foreach (RealmBeatmap b in set.Beatmaps)
|
||||||
|
Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineID == b.OnlineID));
|
||||||
|
Assert.IsTrue(set.Beatmaps.Count > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
|
||||||
|
{
|
||||||
|
const int sleep = 200;
|
||||||
|
|
||||||
|
while (timeout > 0)
|
||||||
|
{
|
||||||
|
Thread.Sleep(sleep);
|
||||||
|
timeout -= sleep;
|
||||||
|
|
||||||
|
if (result())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Fail(failureMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays.BeatmapSet;
|
using osu.Game.Overlays.BeatmapSet;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
namespace osu.Game.Tests.Visual.Online
|
||||||
@ -22,7 +23,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
AddStep("set undownloadable beatmapset with link", () => container.BeatmapSet = new BeatmapSetInfo
|
AddStep("set undownloadable beatmapset with link", () => container.BeatmapSet = new BeatmapSetInfo
|
||||||
{
|
{
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Availability = new BeatmapSetOnlineAvailability
|
Availability = new BeatmapSetOnlineAvailability
|
||||||
{
|
{
|
||||||
@ -40,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
AddStep("set undownloadable beatmapset without link", () => container.BeatmapSet = new BeatmapSetInfo
|
AddStep("set undownloadable beatmapset without link", () => container.BeatmapSet = new BeatmapSetInfo
|
||||||
{
|
{
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Availability = new BeatmapSetOnlineAvailability
|
Availability = new BeatmapSetOnlineAvailability
|
||||||
{
|
{
|
||||||
@ -57,7 +58,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
AddStep("set parts-removed beatmapset with link", () => container.BeatmapSet = new BeatmapSetInfo
|
AddStep("set parts-removed beatmapset with link", () => container.BeatmapSet = new BeatmapSetInfo
|
||||||
{
|
{
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Availability = new BeatmapSetOnlineAvailability
|
Availability = new BeatmapSetOnlineAvailability
|
||||||
{
|
{
|
||||||
@ -75,7 +76,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
AddStep("set normal beatmapset", () => container.BeatmapSet = new BeatmapSetInfo
|
AddStep("set normal beatmapset", () => container.BeatmapSet = new BeatmapSetInfo
|
||||||
{
|
{
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Availability = new BeatmapSetOnlineAvailability
|
Availability = new BeatmapSetOnlineAvailability
|
||||||
{
|
{
|
||||||
|
@ -11,6 +11,7 @@ using osu.Game.Users;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
namespace osu.Game.Tests.Visual.Online
|
||||||
{
|
{
|
||||||
@ -63,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Id = 3,
|
Id = 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Preview = @"https://b.ppy.sh/preview/12345.mp3",
|
Preview = @"https://b.ppy.sh/preview/12345.mp3",
|
||||||
PlayCount = 123,
|
PlayCount = 123,
|
||||||
@ -134,7 +135,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Id = 3,
|
Id = 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Availability = new BeatmapSetOnlineAvailability
|
Availability = new BeatmapSetOnlineAvailability
|
||||||
{
|
{
|
||||||
@ -224,7 +225,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Id = 3,
|
Id = 3,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Covers = new BeatmapSetOnlineCovers(),
|
Covers = new BeatmapSetOnlineCovers(),
|
||||||
},
|
},
|
||||||
@ -309,7 +310,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Id = 3,
|
Id = 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Preview = @"https://b.ppy.sh/preview/123.mp3",
|
Preview = @"https://b.ppy.sh/preview/123.mp3",
|
||||||
HasVideo = true,
|
HasVideo = true,
|
||||||
|
@ -8,6 +8,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.BeatmapSet;
|
using osu.Game.Overlays.BeatmapSet;
|
||||||
using osu.Game.Screens.Select.Details;
|
using osu.Game.Screens.Select.Details;
|
||||||
@ -57,7 +58,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Status = BeatmapSetOnlineStatus.Ranked
|
Status = BeatmapSetOnlineStatus.Ranked
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Online;
|
using osu.Game.Online;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays.BeatmapListing.Panels;
|
using osu.Game.Overlays.BeatmapListing.Panels;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
@ -74,7 +75,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
ID = 1,
|
ID = 1,
|
||||||
OnlineBeatmapSetID = 241526,
|
OnlineBeatmapSetID = 241526,
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Availability = new BeatmapSetOnlineAvailability
|
Availability = new BeatmapSetOnlineAvailability
|
||||||
{
|
{
|
||||||
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays.BeatmapListing.Panels;
|
using osu.Game.Overlays.BeatmapListing.Panels;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
@ -31,7 +32,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Id = 3,
|
Id = 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Availability = new BeatmapSetOnlineAvailability
|
Availability = new BeatmapSetOnlineAvailability
|
||||||
{
|
{
|
||||||
@ -86,7 +87,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Id = 3,
|
Id = 3,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
HasVideo = true,
|
HasVideo = true,
|
||||||
HasStoryboard = true,
|
HasStoryboard = true,
|
||||||
|
@ -3,11 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Online.API;
|
|
||||||
using osu.Game.Online.API.Requests;
|
|
||||||
using osu.Game.Overlays.Profile.Header.Components;
|
using osu.Game.Overlays.Profile.Header.Components;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
@ -16,48 +12,50 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class TestSceneUserProfilePreviousUsernames : OsuTestScene
|
public class TestSceneUserProfilePreviousUsernames : OsuTestScene
|
||||||
{
|
{
|
||||||
[Resolved]
|
private PreviousUsernames container;
|
||||||
private IAPIProvider api { get; set; }
|
|
||||||
|
|
||||||
private readonly Bindable<User> user = new Bindable<User>();
|
[SetUp]
|
||||||
|
public void SetUp() => Schedule(() =>
|
||||||
public TestSceneUserProfilePreviousUsernames()
|
|
||||||
{
|
{
|
||||||
Child = new PreviousUsernames
|
Child = container = new PreviousUsernames
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
User = { BindTarget = user },
|
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
User[] users =
|
[Test]
|
||||||
|
public void TestVisibility()
|
||||||
{
|
{
|
||||||
new User { PreviousUsernames = new[] { "username1" } },
|
AddAssert("Is Hidden", () => container.Alpha == 0);
|
||||||
new User { PreviousUsernames = new[] { "longusername", "longerusername" } },
|
|
||||||
new User { PreviousUsernames = new[] { "test", "angelsim", "verylongusername" } },
|
AddStep("1 username", () => container.User.Value = users[0]);
|
||||||
new User { PreviousUsernames = new[] { "ihavenoidea", "howcani", "makethistext", "anylonger" } },
|
AddUntilStep("Is visible", () => container.Alpha == 1);
|
||||||
new User { PreviousUsernames = Array.Empty<string>() },
|
|
||||||
|
AddStep("2 usernames", () => container.User.Value = users[1]);
|
||||||
|
AddUntilStep("Is visible", () => container.Alpha == 1);
|
||||||
|
|
||||||
|
AddStep("3 usernames", () => container.User.Value = users[2]);
|
||||||
|
AddUntilStep("Is visible", () => container.Alpha == 1);
|
||||||
|
|
||||||
|
AddStep("4 usernames", () => container.User.Value = users[3]);
|
||||||
|
AddUntilStep("Is visible", () => container.Alpha == 1);
|
||||||
|
|
||||||
|
AddStep("No username", () => container.User.Value = users[4]);
|
||||||
|
AddUntilStep("Is hidden", () => container.Alpha == 0);
|
||||||
|
|
||||||
|
AddStep("Null user", () => container.User.Value = users[5]);
|
||||||
|
AddUntilStep("Is hidden", () => container.Alpha == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly User[] users =
|
||||||
|
{
|
||||||
|
new User { Id = 1, PreviousUsernames = new[] { "username1" } },
|
||||||
|
new User { Id = 2, PreviousUsernames = new[] { "longusername", "longerusername" } },
|
||||||
|
new User { Id = 3, PreviousUsernames = new[] { "test", "angelsim", "verylongusername" } },
|
||||||
|
new User { Id = 4, PreviousUsernames = new[] { "ihavenoidea", "howcani", "makethistext", "anylonger" } },
|
||||||
|
new User { Id = 5, PreviousUsernames = Array.Empty<string>() },
|
||||||
null
|
null
|
||||||
};
|
};
|
||||||
|
|
||||||
AddStep("single username", () => user.Value = users[0]);
|
|
||||||
AddStep("two usernames", () => user.Value = users[1]);
|
|
||||||
AddStep("three usernames", () => user.Value = users[2]);
|
|
||||||
AddStep("four usernames", () => user.Value = users[3]);
|
|
||||||
AddStep("no username", () => user.Value = users[4]);
|
|
||||||
AddStep("null user", () => user.Value = users[5]);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
|
|
||||||
AddStep("online user (Angelsim)", () =>
|
|
||||||
{
|
|
||||||
var request = new GetUserRequest(1777162);
|
|
||||||
request.Success += user => this.user.Value = user;
|
|
||||||
api.Queue(request);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
// 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.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Settings
|
||||||
|
{
|
||||||
|
public class TestSceneRestoreDefaultValueButton : OsuTestScene
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private OsuColour colours { get; set; }
|
||||||
|
|
||||||
|
private float scale = 1;
|
||||||
|
|
||||||
|
private readonly Bindable<float> current = new Bindable<float>
|
||||||
|
{
|
||||||
|
Default = default,
|
||||||
|
Value = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBasic()
|
||||||
|
{
|
||||||
|
RestoreDefaultValueButton<float> restoreDefaultValueButton = null;
|
||||||
|
|
||||||
|
AddStep("create button", () => Child = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = colours.GreySeafoam
|
||||||
|
},
|
||||||
|
restoreDefaultValueButton = new RestoreDefaultValueButton<float>
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Scale = new Vector2(scale),
|
||||||
|
Current = current,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
AddSliderStep("set scale", 1, 4, 1, scale =>
|
||||||
|
{
|
||||||
|
this.scale = scale;
|
||||||
|
if (restoreDefaultValueButton != null)
|
||||||
|
restoreDefaultValueButton.Scale = new Vector2(scale);
|
||||||
|
});
|
||||||
|
AddToggleStep("toggle default state", state => current.Value = state ? default : 1);
|
||||||
|
AddToggleStep("toggle disabled state", state => current.Disabled = state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,9 @@ using System.Linq;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
|
||||||
@ -29,9 +32,10 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
Value = "test"
|
Value = "test"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
restoreDefaultValueButton = textBox.ChildrenOfType<RestoreDefaultValueButton<string>>().Single();
|
|
||||||
});
|
});
|
||||||
|
AddUntilStep("wait for loaded", () => textBox.IsLoaded);
|
||||||
|
AddStep("retrieve restore default button", () => restoreDefaultValueButton = textBox.ChildrenOfType<RestoreDefaultValueButton<string>>().Single());
|
||||||
|
|
||||||
AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
|
AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
|
||||||
|
|
||||||
AddStep("change value from default", () => textBox.Current.Value = "non-default");
|
AddStep("change value from default", () => textBox.Current.Value = "non-default");
|
||||||
@ -41,6 +45,48 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
|
AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSetAndClearLabelText()
|
||||||
|
{
|
||||||
|
SettingsTextBox textBox = null;
|
||||||
|
RestoreDefaultValueButton<string> restoreDefaultValueButton = null;
|
||||||
|
OsuTextBox control = null;
|
||||||
|
|
||||||
|
AddStep("create settings item", () =>
|
||||||
|
{
|
||||||
|
Child = textBox = new SettingsTextBox
|
||||||
|
{
|
||||||
|
Current = new Bindable<string>
|
||||||
|
{
|
||||||
|
Default = "test",
|
||||||
|
Value = "test"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
AddUntilStep("wait for loaded", () => textBox.IsLoaded);
|
||||||
|
AddStep("retrieve components", () =>
|
||||||
|
{
|
||||||
|
restoreDefaultValueButton = textBox.ChildrenOfType<RestoreDefaultValueButton<string>>().Single();
|
||||||
|
control = textBox.ChildrenOfType<OsuTextBox>().Single();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("set non-default value", () => restoreDefaultValueButton.Current.Value = "non-default");
|
||||||
|
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
|
||||||
|
|
||||||
|
AddStep("set label", () => textBox.LabelText = "label text");
|
||||||
|
AddAssert("default value button centre aligned to label size", () =>
|
||||||
|
{
|
||||||
|
var label = textBox.ChildrenOfType<OsuSpriteText>().Single(spriteText => spriteText.Text == "label text");
|
||||||
|
return Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, label.DrawHeight, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("clear label", () => textBox.LabelText = default);
|
||||||
|
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
|
||||||
|
|
||||||
|
AddStep("set warning text", () => textBox.WarningText = "This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...");
|
||||||
|
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ensures that the reset to default button uses the correct implementation of IsDefault to determine whether it should be shown or not.
|
/// Ensures that the reset to default button uses the correct implementation of IsDefault to determine whether it should be shown or not.
|
||||||
/// Values have been chosen so that after being set, Value != Default (but they are close enough that the difference is negligible compared to Precision).
|
/// Values have been chosen so that after being set, Value != Default (but they are close enough that the difference is negligible compared to Precision).
|
||||||
@ -64,9 +110,9 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
Precision = 0.1f,
|
Precision = 0.1f,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
restoreDefaultValueButton = sliderBar.ChildrenOfType<RestoreDefaultValueButton<float>>().Single();
|
|
||||||
});
|
});
|
||||||
|
AddUntilStep("wait for loaded", () => sliderBar.IsLoaded);
|
||||||
|
AddStep("retrieve restore default button", () => restoreDefaultValueButton = sliderBar.ChildrenOfType<RestoreDefaultValueButton<float>>().Single());
|
||||||
|
|
||||||
AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
|
AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.BeatmapListing;
|
using osu.Game.Overlays.BeatmapListing;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -111,7 +112,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
|
|
||||||
private static readonly BeatmapSetInfo beatmap_set = new BeatmapSetInfo
|
private static readonly BeatmapSetInfo beatmap_set = new BeatmapSetInfo
|
||||||
{
|
{
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Covers = new BeatmapSetOnlineCovers
|
Covers = new BeatmapSetOnlineCovers
|
||||||
{
|
{
|
||||||
@ -122,7 +123,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
|
|
||||||
private static readonly BeatmapSetInfo no_cover_beatmap_set = new BeatmapSetInfo
|
private static readonly BeatmapSetInfo no_cover_beatmap_set = new BeatmapSetInfo
|
||||||
{
|
{
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Covers = new BeatmapSetOnlineCovers
|
Covers = new BeatmapSetOnlineCovers
|
||||||
{
|
{
|
||||||
|
@ -11,6 +11,7 @@ using osu.Game.Users;
|
|||||||
using System;
|
using System;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.UserInterface
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
{
|
{
|
||||||
@ -69,7 +70,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
Id = 100
|
Id = 100
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Covers = new BeatmapSetOnlineCovers
|
Covers = new BeatmapSetOnlineCovers
|
||||||
{
|
{
|
||||||
@ -90,7 +91,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
Id = 100
|
Id = 100
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Covers = new BeatmapSetOnlineCovers
|
Covers = new BeatmapSetOnlineCovers
|
||||||
{
|
{
|
||||||
@ -115,7 +116,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
Id = 100
|
Id = 100
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Covers = new BeatmapSetOnlineCovers
|
Covers = new BeatmapSetOnlineCovers
|
||||||
{
|
{
|
||||||
@ -136,7 +137,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
Id = 100
|
Id = 100
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Covers = new BeatmapSetOnlineCovers
|
Covers = new BeatmapSetOnlineCovers
|
||||||
{
|
{
|
||||||
|
20
osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs
Normal file
20
osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs
Normal 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 osu.Framework.Graphics;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
|
{
|
||||||
|
public class TestSceneOsuDropdown : ThemeComparisonTestScene
|
||||||
|
{
|
||||||
|
protected override Drawable CreateContent() =>
|
||||||
|
new OsuEnumDropdown<BeatmapSetOnlineStatus>
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Width = 150
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,39 +1,27 @@
|
|||||||
// 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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Overlays;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.UserInterface
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
{
|
{
|
||||||
public class TestSceneOsuTextBox : OsuTestScene
|
public class TestSceneOsuTextBox : ThemeComparisonTestScene
|
||||||
{
|
{
|
||||||
private readonly OsuNumberBox numberBox;
|
private IEnumerable<OsuNumberBox> numberBoxes => this.ChildrenOfType<OsuNumberBox>();
|
||||||
|
|
||||||
public TestSceneOsuTextBox()
|
protected override Drawable CreateContent() => new FillFlowContainer
|
||||||
{
|
{
|
||||||
Child = new Container
|
RelativeSizeAxes = Axes.X,
|
||||||
{
|
AutoSizeAxes = Axes.Y,
|
||||||
Masking = true,
|
|
||||||
CornerRadius = 10f,
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Padding = new MarginPadding(15f),
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Colour = Color4.DarkSlateGray,
|
|
||||||
Alpha = 0.75f,
|
|
||||||
},
|
|
||||||
new FillFlowContainer
|
|
||||||
{
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Direction = FillDirection.Vertical,
|
Direction = FillDirection.Vertical,
|
||||||
Padding = new MarginPadding(50f),
|
Padding = new MarginPadding(50f),
|
||||||
Spacing = new Vector2(0f, 50f),
|
Spacing = new Vector2(0f, 50f),
|
||||||
@ -41,40 +29,39 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
{
|
{
|
||||||
new OsuTextBox
|
new OsuTextBox
|
||||||
{
|
{
|
||||||
Width = 500f,
|
RelativeSizeAxes = Axes.X,
|
||||||
PlaceholderText = "Normal textbox",
|
PlaceholderText = "Normal textbox",
|
||||||
},
|
},
|
||||||
new OsuPasswordTextBox
|
new OsuPasswordTextBox
|
||||||
{
|
{
|
||||||
Width = 500f,
|
RelativeSizeAxes = Axes.X,
|
||||||
PlaceholderText = "Password textbox",
|
PlaceholderText = "Password textbox",
|
||||||
},
|
},
|
||||||
numberBox = new OsuNumberBox
|
new OsuNumberBox
|
||||||
{
|
{
|
||||||
Width = 500f,
|
RelativeSizeAxes = Axes.X,
|
||||||
PlaceholderText = "Number textbox"
|
PlaceholderText = "Number textbox"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestNumberBox()
|
public void TestNumberBox()
|
||||||
{
|
{
|
||||||
clearTextbox(numberBox);
|
AddStep("create themed content", () => CreateThemedContent(OverlayColourScheme.Red));
|
||||||
AddStep("enter numbers", () => numberBox.Text = "987654321");
|
|
||||||
expectedValue(numberBox, "987654321");
|
|
||||||
|
|
||||||
clearTextbox(numberBox);
|
clearTextboxes(numberBoxes);
|
||||||
AddStep("enter text + single number", () => numberBox.Text = "1 hello 2 world 3");
|
AddStep("enter numbers", () => numberBoxes.ForEach(numberBox => numberBox.Text = "987654321"));
|
||||||
expectedValue(numberBox, "123");
|
expectedValue(numberBoxes, "987654321");
|
||||||
|
|
||||||
clearTextbox(numberBox);
|
clearTextboxes(numberBoxes);
|
||||||
|
AddStep("enter text + single number", () => numberBoxes.ForEach(numberBox => numberBox.Text = "1 hello 2 world 3"));
|
||||||
|
expectedValue(numberBoxes, "123");
|
||||||
|
|
||||||
|
clearTextboxes(numberBoxes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearTextbox(OsuTextBox textBox) => AddStep("clear textbox", () => textBox.Text = null);
|
private void clearTextboxes(IEnumerable<OsuTextBox> textBoxes) => AddStep("clear textbox", () => textBoxes.ForEach(textBox => textBox.Text = null));
|
||||||
private void expectedValue(OsuTextBox textBox, string value) => AddAssert("expected textbox value", () => textBox.Text == value);
|
private void expectedValue(IEnumerable<OsuTextBox> textBoxes, string value) => AddAssert("expected textbox value", () => textBoxes.All(textbox => textbox.Text == value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.Drawables;
|
using osu.Game.Beatmaps.Drawables;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.UserInterface
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
@ -22,21 +23,21 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestLocal([Values] BeatmapSetCoverType coverType)
|
public void TestLocal([Values] BeatmapSetCoverType coverType)
|
||||||
{
|
{
|
||||||
AddStep("setup cover", () => Child = new UpdateableBeatmapSetCover(coverType)
|
AddStep("setup cover", () => Child = new UpdateableOnlineBeatmapSetCover(coverType)
|
||||||
{
|
{
|
||||||
BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet,
|
BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet,
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Masking = true,
|
Masking = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for load", () => this.ChildrenOfType<BeatmapSetCover>().SingleOrDefault()?.IsLoaded ?? false);
|
AddUntilStep("wait for load", () => this.ChildrenOfType<OnlineBeatmapSetCover>().SingleOrDefault()?.IsLoaded ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestUnloadAndReload()
|
public void TestUnloadAndReload()
|
||||||
{
|
{
|
||||||
OsuScrollContainer scroll = null;
|
OsuScrollContainer scroll = null;
|
||||||
List<UpdateableBeatmapSetCover> covers = new List<UpdateableBeatmapSetCover>();
|
List<UpdateableOnlineBeatmapSetCover> covers = new List<UpdateableOnlineBeatmapSetCover>();
|
||||||
|
|
||||||
AddStep("setup covers", () =>
|
AddStep("setup covers", () =>
|
||||||
{
|
{
|
||||||
@ -65,7 +66,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
{
|
{
|
||||||
var coverType = coverTypes[i % coverTypes.Count];
|
var coverType = coverTypes[i % coverTypes.Count];
|
||||||
|
|
||||||
var cover = new UpdateableBeatmapSetCover(coverType)
|
var cover = new UpdateableOnlineBeatmapSetCover(coverType)
|
||||||
{
|
{
|
||||||
BeatmapSet = setInfo,
|
BeatmapSet = setInfo,
|
||||||
Height = 100,
|
Height = 100,
|
||||||
@ -84,7 +85,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var loadedCovers = covers.Where(c => c.ChildrenOfType<BeatmapSetCover>().SingleOrDefault()?.IsLoaded ?? false);
|
var loadedCovers = covers.Where(c => c.ChildrenOfType<OnlineBeatmapSetCover>().SingleOrDefault()?.IsLoaded ?? false);
|
||||||
|
|
||||||
AddUntilStep("some loaded", () => loadedCovers.Any());
|
AddUntilStep("some loaded", () => loadedCovers.Any());
|
||||||
AddStep("scroll to end", () => scroll.ScrollToEnd());
|
AddStep("scroll to end", () => scroll.ScrollToEnd());
|
||||||
@ -94,9 +95,9 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestSetNullBeatmapWhileLoading()
|
public void TestSetNullBeatmapWhileLoading()
|
||||||
{
|
{
|
||||||
TestUpdateableBeatmapSetCover updateableCover = null;
|
TestUpdateableOnlineBeatmapSetCover updateableCover = null;
|
||||||
|
|
||||||
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableBeatmapSetCover
|
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover
|
||||||
{
|
{
|
||||||
BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet,
|
BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet,
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
@ -111,10 +112,10 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCoverChangeOnNewBeatmap()
|
public void TestCoverChangeOnNewBeatmap()
|
||||||
{
|
{
|
||||||
TestUpdateableBeatmapSetCover updateableCover = null;
|
TestUpdateableOnlineBeatmapSetCover updateableCover = null;
|
||||||
BeatmapSetCover initialCover = null;
|
OnlineBeatmapSetCover initialCover = null;
|
||||||
|
|
||||||
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableBeatmapSetCover(0)
|
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover(0)
|
||||||
{
|
{
|
||||||
BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg"),
|
BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg"),
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
@ -122,38 +123,38 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
Alpha = 0.4f
|
Alpha = 0.4f
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("cover loaded", () => updateableCover.ChildrenOfType<BeatmapSetCover>().Any());
|
AddUntilStep("cover loaded", () => updateableCover.ChildrenOfType<OnlineBeatmapSetCover>().Any());
|
||||||
AddStep("store initial cover", () => initialCover = updateableCover.ChildrenOfType<BeatmapSetCover>().Single());
|
AddStep("store initial cover", () => initialCover = updateableCover.ChildrenOfType<OnlineBeatmapSetCover>().Single());
|
||||||
AddUntilStep("wait for fade complete", () => initialCover.Alpha == 1);
|
AddUntilStep("wait for fade complete", () => initialCover.Alpha == 1);
|
||||||
|
|
||||||
AddStep("switch beatmap",
|
AddStep("switch beatmap",
|
||||||
() => updateableCover.BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg"));
|
() => updateableCover.BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg"));
|
||||||
AddUntilStep("new cover loaded", () => updateableCover.ChildrenOfType<BeatmapSetCover>().Except(new[] { initialCover }).Any());
|
AddUntilStep("new cover loaded", () => updateableCover.ChildrenOfType<OnlineBeatmapSetCover>().Except(new[] { initialCover }).Any());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BeatmapSetInfo createBeatmapWithCover(string coverUrl) => new BeatmapSetInfo
|
private static BeatmapSetInfo createBeatmapWithCover(string coverUrl) => new BeatmapSetInfo
|
||||||
{
|
{
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Covers = new BeatmapSetOnlineCovers { Cover = coverUrl }
|
Covers = new BeatmapSetOnlineCovers { Cover = coverUrl }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private class TestUpdateableBeatmapSetCover : UpdateableBeatmapSetCover
|
private class TestUpdateableOnlineBeatmapSetCover : UpdateableOnlineBeatmapSetCover
|
||||||
{
|
{
|
||||||
private readonly int loadDelay;
|
private readonly int loadDelay;
|
||||||
|
|
||||||
public TestUpdateableBeatmapSetCover(int loadDelay = 10000)
|
public TestUpdateableOnlineBeatmapSetCover(int loadDelay = 10000)
|
||||||
{
|
{
|
||||||
this.loadDelay = loadDelay;
|
this.loadDelay = loadDelay;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Drawable CreateDrawable(BeatmapSetInfo model)
|
protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model)
|
||||||
{
|
{
|
||||||
if (model == null)
|
if (model == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return new TestBeatmapSetCover(model, loadDelay)
|
return new TestOnlineBeatmapSetCover(model, loadDelay)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -163,11 +164,11 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestBeatmapSetCover : BeatmapSetCover
|
private class TestOnlineBeatmapSetCover : OnlineBeatmapSetCover
|
||||||
{
|
{
|
||||||
private readonly int loadDelay;
|
private readonly int loadDelay;
|
||||||
|
|
||||||
public TestBeatmapSetCover(BeatmapSetInfo set, int loadDelay)
|
public TestOnlineBeatmapSetCover(IBeatmapSetOnlineInfo set, int loadDelay)
|
||||||
: base(set)
|
: base(set)
|
||||||
{
|
{
|
||||||
this.loadDelay = loadDelay;
|
this.loadDelay = loadDelay;
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
// 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.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
|
{
|
||||||
|
public abstract class ThemeComparisonTestScene : OsuGridTestScene
|
||||||
|
{
|
||||||
|
protected ThemeComparisonTestScene()
|
||||||
|
: base(1, 2)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuColour colours)
|
||||||
|
{
|
||||||
|
Cell(0, 0).AddRange(new[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = colours.GreySeafoam
|
||||||
|
},
|
||||||
|
CreateContent()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void CreateThemedContent(OverlayColourScheme colourScheme)
|
||||||
|
{
|
||||||
|
var colourProvider = new OverlayColourProvider(colourScheme);
|
||||||
|
|
||||||
|
Cell(0, 1).Clear();
|
||||||
|
Cell(0, 1).Add(new DependencyProvidingContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
CachedDependencies = new (Type, object)[]
|
||||||
|
{
|
||||||
|
(typeof(OverlayColourProvider), colourProvider)
|
||||||
|
},
|
||||||
|
Children = new[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = colourProvider.Background4
|
||||||
|
},
|
||||||
|
CreateContent()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Drawable CreateContent();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAllColourSchemes()
|
||||||
|
{
|
||||||
|
foreach (var scheme in Enum.GetValues(typeof(OverlayColourScheme)).Cast<OverlayColourScheme>())
|
||||||
|
AddStep($"set {scheme} scheme", () => CreateThemedContent(scheme));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,6 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Framework.Localisation;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.Drawables;
|
using osu.Game.Beatmaps.Drawables;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
@ -21,7 +20,8 @@ namespace osu.Game.Tournament.Components
|
|||||||
{
|
{
|
||||||
public class TournamentBeatmapPanel : CompositeDrawable
|
public class TournamentBeatmapPanel : CompositeDrawable
|
||||||
{
|
{
|
||||||
public readonly BeatmapInfo BeatmapInfo;
|
public readonly IBeatmapInfo BeatmapInfo;
|
||||||
|
|
||||||
private readonly string mod;
|
private readonly string mod;
|
||||||
|
|
||||||
private const float horizontal_padding = 10;
|
private const float horizontal_padding = 10;
|
||||||
@ -32,12 +32,13 @@ namespace osu.Game.Tournament.Components
|
|||||||
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
|
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
|
||||||
private Box flash;
|
private Box flash;
|
||||||
|
|
||||||
public TournamentBeatmapPanel(BeatmapInfo beatmapInfo, string mod = null)
|
public TournamentBeatmapPanel(IBeatmapInfo beatmapInfo, string mod = null)
|
||||||
{
|
{
|
||||||
if (beatmapInfo == null) throw new ArgumentNullException(nameof(beatmapInfo));
|
if (beatmapInfo == null) throw new ArgumentNullException(nameof(beatmapInfo));
|
||||||
|
|
||||||
BeatmapInfo = beatmapInfo;
|
BeatmapInfo = beatmapInfo;
|
||||||
this.mod = mod;
|
this.mod = mod;
|
||||||
|
|
||||||
Width = 400;
|
Width = 400;
|
||||||
Height = HEIGHT;
|
Height = HEIGHT;
|
||||||
}
|
}
|
||||||
@ -57,11 +58,11 @@ namespace osu.Game.Tournament.Components
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = Color4.Black,
|
Colour = Color4.Black,
|
||||||
},
|
},
|
||||||
new UpdateableBeatmapSetCover
|
new UpdateableOnlineBeatmapSetCover
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = OsuColour.Gray(0.5f),
|
Colour = OsuColour.Gray(0.5f),
|
||||||
BeatmapSet = BeatmapInfo.BeatmapSet,
|
BeatmapSet = BeatmapInfo.BeatmapSet as IBeatmapSetOnlineInfo,
|
||||||
},
|
},
|
||||||
new FillFlowContainer
|
new FillFlowContainer
|
||||||
{
|
{
|
||||||
@ -74,9 +75,7 @@ namespace osu.Game.Tournament.Components
|
|||||||
{
|
{
|
||||||
new TournamentSpriteText
|
new TournamentSpriteText
|
||||||
{
|
{
|
||||||
Text = new RomanisableString(
|
Text = BeatmapInfo.GetDisplayTitleRomanisable(false),
|
||||||
$"{BeatmapInfo.Metadata.ArtistUnicode ?? BeatmapInfo.Metadata.Artist} - {BeatmapInfo.Metadata.TitleUnicode ?? BeatmapInfo.Metadata.Title}",
|
|
||||||
$"{BeatmapInfo.Metadata.Artist} - {BeatmapInfo.Metadata.Title}"),
|
|
||||||
Font = OsuFont.Torus.With(weight: FontWeight.Bold),
|
Font = OsuFont.Torus.With(weight: FontWeight.Bold),
|
||||||
},
|
},
|
||||||
new FillFlowContainer
|
new FillFlowContainer
|
||||||
@ -93,7 +92,7 @@ namespace osu.Game.Tournament.Components
|
|||||||
},
|
},
|
||||||
new TournamentSpriteText
|
new TournamentSpriteText
|
||||||
{
|
{
|
||||||
Text = BeatmapInfo.Metadata.AuthorString,
|
Text = BeatmapInfo.Metadata?.Author,
|
||||||
Padding = new MarginPadding { Right = 20 },
|
Padding = new MarginPadding { Right = 20 },
|
||||||
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14)
|
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14)
|
||||||
},
|
},
|
||||||
@ -105,7 +104,7 @@ namespace osu.Game.Tournament.Components
|
|||||||
},
|
},
|
||||||
new TournamentSpriteText
|
new TournamentSpriteText
|
||||||
{
|
{
|
||||||
Text = BeatmapInfo.Version,
|
Text = BeatmapInfo.DifficultyName,
|
||||||
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14)
|
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -149,7 +148,7 @@ namespace osu.Game.Tournament.Components
|
|||||||
|
|
||||||
private void updateState()
|
private void updateState()
|
||||||
{
|
{
|
||||||
var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == BeatmapInfo.OnlineBeatmapID);
|
var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == BeatmapInfo.OnlineID);
|
||||||
|
|
||||||
bool doFlash = found != choice;
|
bool doFlash = found != choice;
|
||||||
choice = found;
|
choice = found;
|
||||||
|
@ -147,11 +147,11 @@ namespace osu.Game.Tournament.Screens.MapPool
|
|||||||
|
|
||||||
if (map != null)
|
if (map != null)
|
||||||
{
|
{
|
||||||
if (e.Button == MouseButton.Left && map.BeatmapInfo.OnlineBeatmapID != null)
|
if (e.Button == MouseButton.Left && map.BeatmapInfo.OnlineID > 0)
|
||||||
addForBeatmap(map.BeatmapInfo.OnlineBeatmapID.Value);
|
addForBeatmap(map.BeatmapInfo.OnlineID);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.BeatmapInfo.OnlineBeatmapID);
|
var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.BeatmapInfo.OnlineID);
|
||||||
|
|
||||||
if (existing != null)
|
if (existing != null)
|
||||||
{
|
{
|
||||||
|
@ -16,14 +16,19 @@ namespace osu.Game.Beatmaps
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields.
|
/// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapInfo beatmapInfo)
|
public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapInfo beatmapInfo, bool includeDifficultyName = true)
|
||||||
{
|
{
|
||||||
var metadata = getClosestMetadata(beatmapInfo).GetDisplayTitleRomanisable();
|
var metadata = getClosestMetadata(beatmapInfo).GetDisplayTitleRomanisable();
|
||||||
var versionString = getVersionString(beatmapInfo);
|
|
||||||
|
|
||||||
|
if (includeDifficultyName)
|
||||||
|
{
|
||||||
|
var versionString = getVersionString(beatmapInfo);
|
||||||
return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim());
|
return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new RomanisableString($"{metadata.GetPreferred(true)}".Trim(), $"{metadata.GetPreferred(false)}".Trim());
|
||||||
|
}
|
||||||
|
|
||||||
public static string[] GetSearchableTerms(this IBeatmapInfo beatmapInfo) => new[]
|
public static string[] GetSearchableTerms(this IBeatmapInfo beatmapInfo) => new[]
|
||||||
{
|
{
|
||||||
beatmapInfo.DifficultyName
|
beatmapInfo.DifficultyName
|
||||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps
|
|||||||
/// Handles general operations related to global beatmap management.
|
/// Handles general operations related to global beatmap management.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ExcludeFromDynamicCompile]
|
[ExcludeFromDynamicCompile]
|
||||||
public class BeatmapManager : IModelDownloader<BeatmapSetInfo>, IModelManager<BeatmapSetInfo>, IModelFileManager<BeatmapSetInfo, BeatmapSetFileInfo>, ICanAcceptFiles, IWorkingBeatmapCache, IDisposable
|
public class BeatmapManager : IModelDownloader<BeatmapSetInfo>, IModelManager<BeatmapSetInfo>, IModelFileManager<BeatmapSetInfo, BeatmapSetFileInfo>, IWorkingBeatmapCache, IDisposable
|
||||||
{
|
{
|
||||||
private readonly BeatmapModelManager beatmapModelManager;
|
private readonly BeatmapModelManager beatmapModelManager;
|
||||||
private readonly BeatmapModelDownloader beatmapModelDownloader;
|
private readonly BeatmapModelDownloader beatmapModelDownloader;
|
||||||
|
@ -123,15 +123,15 @@ namespace osu.Game.Beatmaps
|
|||||||
// check if a set already exists with the same online id, delete if it does.
|
// check if a set already exists with the same online id, delete if it does.
|
||||||
if (beatmapSet.OnlineBeatmapSetID != null)
|
if (beatmapSet.OnlineBeatmapSetID != null)
|
||||||
{
|
{
|
||||||
var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID);
|
var existingSetWithSameOnlineID = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID);
|
||||||
|
|
||||||
if (existingOnlineId != null)
|
if (existingSetWithSameOnlineID != null)
|
||||||
{
|
{
|
||||||
Delete(existingOnlineId);
|
Delete(existingSetWithSameOnlineID);
|
||||||
|
|
||||||
// in order to avoid a unique key constraint, immediately remove the online ID from the previous set.
|
// in order to avoid a unique key constraint, immediately remove the online ID from the previous set.
|
||||||
existingOnlineId.OnlineBeatmapSetID = null;
|
existingSetWithSameOnlineID.OnlineBeatmapSetID = null;
|
||||||
foreach (var b in existingOnlineId.Beatmaps)
|
foreach (var b in existingSetWithSameOnlineID.Beatmaps)
|
||||||
b.OnlineBeatmapID = null;
|
b.OnlineBeatmapID = null;
|
||||||
|
|
||||||
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted.");
|
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted.");
|
||||||
|
@ -83,9 +83,9 @@ namespace osu.Game.Beatmaps
|
|||||||
if (res != null)
|
if (res != null)
|
||||||
{
|
{
|
||||||
beatmapInfo.Status = res.Status;
|
beatmapInfo.Status = res.Status;
|
||||||
beatmapInfo.BeatmapSet.Status = res.BeatmapSet.Status;
|
beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapSetOnlineStatus.None;
|
||||||
beatmapInfo.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
|
beatmapInfo.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
|
||||||
beatmapInfo.OnlineBeatmapID = res.OnlineBeatmapID;
|
beatmapInfo.OnlineBeatmapID = res.OnlineID;
|
||||||
|
|
||||||
if (beatmapInfo.Metadata != null)
|
if (beatmapInfo.Metadata != null)
|
||||||
beatmapInfo.Metadata.AuthorID = res.AuthorID;
|
beatmapInfo.Metadata.AuthorID = res.AuthorID;
|
||||||
@ -93,7 +93,7 @@ namespace osu.Game.Beatmaps
|
|||||||
if (beatmapInfo.BeatmapSet.Metadata != null)
|
if (beatmapInfo.BeatmapSet.Metadata != null)
|
||||||
beatmapInfo.BeatmapSet.Metadata.AuthorID = res.AuthorID;
|
beatmapInfo.BeatmapSet.Metadata.AuthorID = res.AuthorID;
|
||||||
|
|
||||||
logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}.");
|
logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineID}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
@ -6,13 +6,15 @@ using System.Collections.Generic;
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps
|
namespace osu.Game.Beatmaps
|
||||||
{
|
{
|
||||||
[ExcludeFromDynamicCompile]
|
[ExcludeFromDynamicCompile]
|
||||||
public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles<BeatmapSetFileInfo>, ISoftDelete, IEquatable<BeatmapSetInfo>, IBeatmapSetInfo
|
public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles<BeatmapSetFileInfo>, ISoftDelete, IEquatable<BeatmapSetInfo>, IBeatmapSetInfo, IBeatmapSetOnlineInfo
|
||||||
{
|
{
|
||||||
public int ID { get; set; }
|
public int ID { get; set; }
|
||||||
|
|
||||||
@ -26,8 +28,6 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
public DateTimeOffset DateAdded { get; set; }
|
public DateTimeOffset DateAdded { get; set; }
|
||||||
|
|
||||||
public BeatmapSetOnlineStatus Status { get; set; } = BeatmapSetOnlineStatus.None;
|
|
||||||
|
|
||||||
public BeatmapMetadata Metadata { get; set; }
|
public BeatmapMetadata Metadata { get; set; }
|
||||||
|
|
||||||
public List<BeatmapInfo> Beatmaps { get; set; }
|
public List<BeatmapInfo> Beatmaps { get; set; }
|
||||||
@ -36,7 +36,7 @@ namespace osu.Game.Beatmaps
|
|||||||
public List<BeatmapSetFileInfo> Files { get; set; } = new List<BeatmapSetFileInfo>();
|
public List<BeatmapSetFileInfo> Files { get; set; } = new List<BeatmapSetFileInfo>();
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public BeatmapSetOnlineInfo OnlineInfo { get; set; }
|
public APIBeatmapSet OnlineInfo { get; set; }
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public BeatmapSetMetrics Metrics { get; set; }
|
public BeatmapSetMetrics Metrics { get; set; }
|
||||||
@ -102,5 +102,77 @@ namespace osu.Game.Beatmaps
|
|||||||
IEnumerable<INamedFileUsage> IBeatmapSetInfo.Files => Files;
|
IEnumerable<INamedFileUsage> IBeatmapSetInfo.Files => Files;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Delegation for IBeatmapSetOnlineInfo
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public DateTimeOffset Submitted => OnlineInfo.Submitted;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public DateTimeOffset? Ranked => OnlineInfo.Ranked;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public DateTimeOffset? LastUpdated => OnlineInfo.LastUpdated;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public BeatmapSetOnlineStatus Status { get; set; } = BeatmapSetOnlineStatus.None;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool HasExplicitContent => OnlineInfo.HasExplicitContent;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool HasVideo => OnlineInfo.HasVideo;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool HasStoryboard => OnlineInfo.HasStoryboard;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public BeatmapSetOnlineCovers Covers => OnlineInfo.Covers;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public string Preview => OnlineInfo.Preview;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public double BPM => OnlineInfo.BPM;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public int PlayCount => OnlineInfo.PlayCount;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public int FavouriteCount => OnlineInfo.FavouriteCount;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool HasFavourited => OnlineInfo.HasFavourited;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public BeatmapSetOnlineAvailability Availability => OnlineInfo.Availability;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public BeatmapSetOnlineGenre Genre => OnlineInfo.Genre;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public BeatmapSetOnlineLanguage Language => OnlineInfo.Language;
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
[JsonIgnore]
|
||||||
|
public int? TrackId => OnlineInfo?.TrackId;
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
16
osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs
Normal file
16
osu.Game/Beatmaps/BeatmapSetOnlineAvailability.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// 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 Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace osu.Game.Beatmaps
|
||||||
|
{
|
||||||
|
public struct BeatmapSetOnlineAvailability
|
||||||
|
{
|
||||||
|
[JsonProperty(@"download_disabled")]
|
||||||
|
public bool DownloadDisabled { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"more_information")]
|
||||||
|
public string ExternalLink { get; set; }
|
||||||
|
}
|
||||||
|
}
|
25
osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs
Normal file
25
osu.Game/Beatmaps/BeatmapSetOnlineCovers.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// 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 Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace osu.Game.Beatmaps
|
||||||
|
{
|
||||||
|
public struct BeatmapSetOnlineCovers
|
||||||
|
{
|
||||||
|
public string CoverLowRes { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"cover@2x")]
|
||||||
|
public string Cover { get; set; }
|
||||||
|
|
||||||
|
public string CardLowRes { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"card@2x")]
|
||||||
|
public string Card { get; set; }
|
||||||
|
|
||||||
|
public string ListLowRes { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"list@2x")]
|
||||||
|
public string List { get; set; }
|
||||||
|
}
|
||||||
|
}
|
11
osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs
Normal file
11
osu.Game/Beatmaps/BeatmapSetOnlineGenre.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
namespace osu.Game.Beatmaps
|
||||||
|
{
|
||||||
|
public struct BeatmapSetOnlineGenre
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
}
|
11
osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs
Normal file
11
osu.Game/Beatmaps/BeatmapSetOnlineLanguage.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
namespace osu.Game.Beatmaps
|
||||||
|
{
|
||||||
|
public struct BeatmapSetOnlineLanguage
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -9,12 +9,12 @@ using osu.Framework.Graphics.Textures;
|
|||||||
namespace osu.Game.Beatmaps.Drawables
|
namespace osu.Game.Beatmaps.Drawables
|
||||||
{
|
{
|
||||||
[LongRunningLoad]
|
[LongRunningLoad]
|
||||||
public class BeatmapSetCover : Sprite
|
public class OnlineBeatmapSetCover : Sprite
|
||||||
{
|
{
|
||||||
private readonly BeatmapSetInfo set;
|
private readonly IBeatmapSetOnlineInfo set;
|
||||||
private readonly BeatmapSetCoverType type;
|
private readonly BeatmapSetCoverType type;
|
||||||
|
|
||||||
public BeatmapSetCover(BeatmapSetInfo set, BeatmapSetCoverType type = BeatmapSetCoverType.Cover)
|
public OnlineBeatmapSetCover(IBeatmapSetOnlineInfo set, BeatmapSetCoverType type = BeatmapSetCoverType.Cover)
|
||||||
{
|
{
|
||||||
if (set == null)
|
if (set == null)
|
||||||
throw new ArgumentNullException(nameof(set));
|
throw new ArgumentNullException(nameof(set));
|
||||||
@ -31,15 +31,15 @@ namespace osu.Game.Beatmaps.Drawables
|
|||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case BeatmapSetCoverType.Cover:
|
case BeatmapSetCoverType.Cover:
|
||||||
resource = set.OnlineInfo.Covers.Cover;
|
resource = set.Covers.Cover;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case BeatmapSetCoverType.Card:
|
case BeatmapSetCoverType.Card:
|
||||||
resource = set.OnlineInfo.Covers.Card;
|
resource = set.Covers.Card;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case BeatmapSetCoverType.List:
|
case BeatmapSetCoverType.List:
|
||||||
resource = set.OnlineInfo.Covers.List;
|
resource = set.Covers.List;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps.Drawables
|
|||||||
{
|
{
|
||||||
// prefer online cover where available.
|
// prefer online cover where available.
|
||||||
if (model?.BeatmapSet?.OnlineInfo != null)
|
if (model?.BeatmapSet?.OnlineInfo != null)
|
||||||
return new BeatmapSetCover(model.BeatmapSet, beatmapSetCoverType);
|
return new OnlineBeatmapSetCover(model.BeatmapSet, beatmapSetCoverType);
|
||||||
|
|
||||||
return model?.ID > 0
|
return model?.ID > 0
|
||||||
? new BeatmapBackgroundSprite(beatmaps.GetWorkingBeatmap(model))
|
? new BeatmapBackgroundSprite(beatmaps.GetWorkingBeatmap(model))
|
||||||
|
@ -9,11 +9,11 @@ using osu.Game.Graphics;
|
|||||||
|
|
||||||
namespace osu.Game.Beatmaps.Drawables
|
namespace osu.Game.Beatmaps.Drawables
|
||||||
{
|
{
|
||||||
public class UpdateableBeatmapSetCover : ModelBackedDrawable<BeatmapSetInfo>
|
public class UpdateableOnlineBeatmapSetCover : ModelBackedDrawable<IBeatmapSetOnlineInfo>
|
||||||
{
|
{
|
||||||
private readonly BeatmapSetCoverType coverType;
|
private readonly BeatmapSetCoverType coverType;
|
||||||
|
|
||||||
public BeatmapSetInfo BeatmapSet
|
public IBeatmapSetOnlineInfo BeatmapSet
|
||||||
{
|
{
|
||||||
get => Model;
|
get => Model;
|
||||||
set => Model = value;
|
set => Model = value;
|
||||||
@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.Drawables
|
|||||||
set => base.Masking = value;
|
set => base.Masking = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UpdateableBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover)
|
public UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover)
|
||||||
{
|
{
|
||||||
this.coverType = coverType;
|
this.coverType = coverType;
|
||||||
|
|
||||||
@ -43,12 +43,12 @@ namespace osu.Game.Beatmaps.Drawables
|
|||||||
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
|
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
|
||||||
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad);
|
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad);
|
||||||
|
|
||||||
protected override Drawable CreateDrawable(BeatmapSetInfo model)
|
protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model)
|
||||||
{
|
{
|
||||||
if (model == null)
|
if (model == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return new BeatmapSetCover(model, coverType)
|
return new OnlineBeatmapSetCover(model, coverType)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
@ -1,139 +1,101 @@
|
|||||||
// 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;
|
using System;
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps
|
namespace osu.Game.Beatmaps
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Beatmap set info retrieved for previewing locally without having the set downloaded.
|
/// Beatmap set info retrieved for previewing locally without having the set downloaded.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class BeatmapSetOnlineInfo
|
public interface IBeatmapSetOnlineInfo
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The date this beatmap set was submitted to the online listing.
|
/// The date this beatmap set was submitted to the online listing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTimeOffset Submitted { get; set; }
|
DateTimeOffset Submitted { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The date this beatmap set was ranked.
|
/// The date this beatmap set was ranked.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTimeOffset? Ranked { get; set; }
|
DateTimeOffset? Ranked { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The date this beatmap set was last updated.
|
/// The date this beatmap set was last updated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTimeOffset? LastUpdated { get; set; }
|
DateTimeOffset? LastUpdated { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The status of this beatmap set.
|
/// The status of this beatmap set.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BeatmapSetOnlineStatus Status { get; set; }
|
BeatmapSetOnlineStatus Status { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not this beatmap set has explicit content.
|
/// Whether or not this beatmap set has explicit content.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasExplicitContent { get; set; }
|
bool HasExplicitContent { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not this beatmap set has a background video.
|
/// Whether or not this beatmap set has a background video.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasVideo { get; set; }
|
bool HasVideo { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not this beatmap set has a storyboard.
|
/// Whether or not this beatmap set has a storyboard.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasStoryboard { get; set; }
|
bool HasStoryboard { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The different sizes of cover art for this beatmap set.
|
/// The different sizes of cover art for this beatmap set.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BeatmapSetOnlineCovers Covers { get; set; }
|
BeatmapSetOnlineCovers Covers { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A small sample clip of this beatmap set's song.
|
/// A small sample clip of this beatmap set's song.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Preview { get; set; }
|
string Preview { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The beats per minute of this beatmap set's song.
|
/// The beats per minute of this beatmap set's song.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double BPM { get; set; }
|
double BPM { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The amount of plays this beatmap set has.
|
/// The amount of plays this beatmap set has.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int PlayCount { get; set; }
|
int PlayCount { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The amount of people who have favourited this beatmap set.
|
/// The amount of people who have favourited this beatmap set.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int FavouriteCount { get; set; }
|
int FavouriteCount { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this beatmap set has been favourited by the current user.
|
/// Whether this beatmap set has been favourited by the current user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasFavourited { get; set; }
|
bool HasFavourited { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The availability of this beatmap set.
|
/// The availability of this beatmap set.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BeatmapSetOnlineAvailability Availability { get; set; }
|
BeatmapSetOnlineAvailability Availability { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The song genre of this beatmap set.
|
/// The song genre of this beatmap set.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BeatmapSetOnlineGenre Genre { get; set; }
|
BeatmapSetOnlineGenre Genre { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The song language of this beatmap set.
|
/// The song language of this beatmap set.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BeatmapSetOnlineLanguage Language { get; set; }
|
BeatmapSetOnlineLanguage Language { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The track ID of this beatmap set.
|
/// The track ID of this beatmap set.
|
||||||
/// Non-null only if the track is linked to a featured artist track entry.
|
/// Non-null only if the track is linked to a featured artist track entry.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? TrackId { get; set; }
|
int? TrackId { get; }
|
||||||
}
|
|
||||||
|
|
||||||
public class BeatmapSetOnlineGenre
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BeatmapSetOnlineLanguage
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BeatmapSetOnlineCovers
|
|
||||||
{
|
|
||||||
public string CoverLowRes { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(@"cover@2x")]
|
|
||||||
public string Cover { get; set; }
|
|
||||||
|
|
||||||
public string CardLowRes { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(@"card@2x")]
|
|
||||||
public string Card { get; set; }
|
|
||||||
|
|
||||||
public string ListLowRes { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(@"list@2x")]
|
|
||||||
public string List { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BeatmapSetOnlineAvailability
|
|
||||||
{
|
|
||||||
[JsonProperty(@"download_disabled")]
|
|
||||||
public bool DownloadDisabled { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(@"more_information")]
|
|
||||||
public string ExternalLink { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -181,7 +181,11 @@ namespace osu.Game.Collections
|
|||||||
MaxHeight = 200;
|
MaxHeight = 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownMenuItem(item);
|
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownMenuItem(item)
|
||||||
|
{
|
||||||
|
BackgroundColourHover = HoverColour,
|
||||||
|
BackgroundColourSelected = SelectionColour
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem
|
protected class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem
|
||||||
|
@ -6,10 +6,13 @@ using System.Diagnostics;
|
|||||||
using osu.Framework.Configuration;
|
using osu.Framework.Configuration;
|
||||||
using osu.Framework.Configuration.Tracking;
|
using osu.Framework.Configuration.Tracking;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Extensions.LocalisationExtensions;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Input;
|
using osu.Game.Input;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
@ -185,20 +188,52 @@ namespace osu.Game.Configuration
|
|||||||
|
|
||||||
return new TrackedSettings
|
return new TrackedSettings
|
||||||
{
|
{
|
||||||
new TrackedSetting<bool>(OsuSetting.MouseDisableButtons, v => new SettingDescription(!v, "gameplay mouse buttons", v ? "disabled" : "enabled", LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons))),
|
new TrackedSetting<bool>(OsuSetting.MouseDisableButtons, disabledState => new SettingDescription(
|
||||||
new TrackedSetting<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode, m => new SettingDescription(m, "HUD Visibility", m.GetDescription(), $"cycle: {LookupKeyBindings(GlobalAction.ToggleInGameInterface)} quick view: {LookupKeyBindings(GlobalAction.HoldForHUD)}")),
|
rawValue: !disabledState,
|
||||||
new TrackedSetting<ScalingMode>(OsuSetting.Scaling, m => new SettingDescription(m, "scaling", m.GetDescription())),
|
name: GlobalActionKeyBindingStrings.ToggleGameplayMouseButtons,
|
||||||
new TrackedSetting<int>(OsuSetting.Skin, m =>
|
value: disabledState ? CommonStrings.Disabled.ToLower() : CommonStrings.Enabled.ToLower(),
|
||||||
|
shortcut: LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons))
|
||||||
|
),
|
||||||
|
new TrackedSetting<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode, visibilityMode => new SettingDescription(
|
||||||
|
rawValue: visibilityMode,
|
||||||
|
name: GameplaySettingsStrings.HUDVisibilityMode,
|
||||||
|
value: visibilityMode.GetLocalisableDescription(),
|
||||||
|
shortcut: new TranslatableString(@"_", @"{0}: {1} {2}: {3}",
|
||||||
|
GlobalActionKeyBindingStrings.ToggleInGameInterface,
|
||||||
|
LookupKeyBindings(GlobalAction.ToggleInGameInterface),
|
||||||
|
GlobalActionKeyBindingStrings.HoldForHUD,
|
||||||
|
LookupKeyBindings(GlobalAction.HoldForHUD)))
|
||||||
|
),
|
||||||
|
new TrackedSetting<ScalingMode>(OsuSetting.Scaling, scalingMode => new SettingDescription(
|
||||||
|
rawValue: scalingMode,
|
||||||
|
name: GraphicsSettingsStrings.ScreenScaling,
|
||||||
|
value: scalingMode.GetLocalisableDescription()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new TrackedSetting<int>(OsuSetting.Skin, skin =>
|
||||||
{
|
{
|
||||||
string skinName = LookupSkinName(m) ?? string.Empty;
|
string skinName = LookupSkinName(skin) ?? string.Empty;
|
||||||
return new SettingDescription(skinName, "skin", skinName, $"random: {LookupKeyBindings(GlobalAction.RandomSkin)}");
|
|
||||||
})
|
return new SettingDescription(
|
||||||
|
rawValue: skinName,
|
||||||
|
name: SkinSettingsStrings.SkinSectionHeader,
|
||||||
|
value: skinName,
|
||||||
|
shortcut: $"{GlobalActionKeyBindingStrings.RandomSkin}: {LookupKeyBindings(GlobalAction.RandomSkin)}"
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
new TrackedSetting<float>(OsuSetting.UIScale, scale => new SettingDescription(
|
||||||
|
rawValue: scale,
|
||||||
|
name: GraphicsSettingsStrings.UIScaling,
|
||||||
|
value: $"{scale:N2}x"
|
||||||
|
// TODO: implement lookup for framework platform key bindings
|
||||||
|
)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public Func<int, string> LookupSkinName { private get; set; }
|
public Func<int, string> LookupSkinName { private get; set; }
|
||||||
|
|
||||||
public Func<GlobalAction, string> LookupKeyBindings { get; set; }
|
public Func<GlobalAction, LocalisableString> LookupKeyBindings { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// IMPORTANT: These are used in user configuration files.
|
// IMPORTANT: These are used in user configuration files.
|
||||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Database
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TModel">The model type.</typeparam>
|
/// <typeparam name="TModel">The model type.</typeparam>
|
||||||
/// <typeparam name="TFileModel">The associated file join type.</typeparam>
|
/// <typeparam name="TFileModel">The associated file join type.</typeparam>
|
||||||
public abstract class ArchiveModelManager<TModel, TFileModel> : ICanAcceptFiles, IModelManager<TModel>, IModelFileManager<TModel, TFileModel>
|
public abstract class ArchiveModelManager<TModel, TFileModel> : IModelManager<TModel>, IModelFileManager<TModel, TFileModel>
|
||||||
where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
|
where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
|
||||||
where TFileModel : class, INamedFileInfo, new()
|
where TFileModel : class, INamedFileInfo, new()
|
||||||
{
|
{
|
||||||
|
@ -8,8 +8,12 @@ namespace osu.Game.Database
|
|||||||
public interface IHasOnlineID
|
public interface IHasOnlineID
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The server-side ID representing this instance, if one exists. -1 denotes a missing ID.
|
/// The server-side ID representing this instance, if one exists. Any value 0 or less denotes a missing ID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Generally we use -1 when specifying "missing" in code, but values of 0 are also considered missing as the online source
|
||||||
|
/// is generally a MySQL autoincrement value, which can never be 0.
|
||||||
|
/// </remarks>
|
||||||
int OnlineID { get; }
|
int OnlineID { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,21 +13,9 @@ namespace osu.Game.Database
|
|||||||
/// A class which handles importing of associated models to the game store.
|
/// A class which handles importing of associated models to the game store.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TModel">The model type.</typeparam>
|
/// <typeparam name="TModel">The model type.</typeparam>
|
||||||
public interface IModelImporter<TModel> : IPostNotifications, IPostImports<TModel>
|
public interface IModelImporter<TModel> : IPostNotifications, IPostImports<TModel>, ICanAcceptFiles
|
||||||
where TModel : class
|
where TModel : class
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Import one or more <typeparamref name="TModel"/> items from filesystem <paramref name="paths"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This will be treated as a low priority import if more than one path is specified; use <see cref="ArchiveModelManager{TModel,TFileModel}.Import(osu.Game.Database.ImportTask[])"/> to always import at standard priority.
|
|
||||||
/// This will post notifications tracking progress.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="paths">One or more archive locations on disk.</param>
|
|
||||||
Task Import(params string[] paths);
|
|
||||||
|
|
||||||
Task Import(params ImportTask[] tasks);
|
|
||||||
|
|
||||||
Task<IEnumerable<ILive<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks);
|
Task<IEnumerable<ILive<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
namespace osu.Game.Database
|
namespace osu.Game.Database
|
||||||
{
|
{
|
||||||
public interface IPostImports<out TModel>
|
public interface IPostImports<out TModel>
|
||||||
@ -12,6 +14,6 @@ namespace osu.Game.Database
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fired when the user requests to view the resulting import.
|
/// Fired when the user requests to view the resulting import.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Action<IEnumerable<ILive<TModel>>> PostImport { set; }
|
public Action<IEnumerable<ILive<TModel>>>? PostImport { set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,27 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
|
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
|
||||||
Filename += realm_extension;
|
Filename += realm_extension;
|
||||||
|
|
||||||
|
cleanupPendingDeletions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupPendingDeletions()
|
||||||
|
{
|
||||||
|
using (var realm = CreateContext())
|
||||||
|
using (var transaction = realm.BeginWrite())
|
||||||
|
{
|
||||||
|
var pendingDeleteSets = realm.All<RealmBeatmapSet>().Where(s => s.DeletePending);
|
||||||
|
|
||||||
|
foreach (var s in pendingDeleteSets)
|
||||||
|
{
|
||||||
|
foreach (var b in s.Beatmaps)
|
||||||
|
realm.Remove(b);
|
||||||
|
|
||||||
|
realm.Remove(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -235,11 +235,18 @@ namespace osu.Game.Graphics
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly Color4 Blue3 = Color4Extensions.FromHex(@"3399cc");
|
public readonly Color4 Blue3 = Color4Extensions.FromHex(@"3399cc");
|
||||||
|
|
||||||
|
public readonly Color4 Lime0 = Color4Extensions.FromHex(@"ccff99");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Equivalent to <see cref="OverlayColourProvider.Lime"/>'s <see cref="OverlayColourProvider.Colour1"/>.
|
/// Equivalent to <see cref="OverlayColourProvider.Lime"/>'s <see cref="OverlayColourProvider.Colour1"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly Color4 Lime1 = Color4Extensions.FromHex(@"b2ff66");
|
public readonly Color4 Lime1 = Color4Extensions.FromHex(@"b2ff66");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Equivalent to <see cref="OverlayColourProvider.Lime"/>'s <see cref="OverlayColourProvider.Colour3"/>.
|
||||||
|
/// </summary>
|
||||||
|
public readonly Color4 Lime3 = Color4Extensions.FromHex(@"7fcc33");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Equivalent to <see cref="OverlayColourProvider.Orange"/>'s <see cref="OverlayColourProvider.Colour1"/>.
|
/// Equivalent to <see cref="OverlayColourProvider.Orange"/>'s <see cref="OverlayColourProvider.Colour1"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
@ -8,6 +10,7 @@ using osu.Framework.Platform;
|
|||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterface
|
namespace osu.Game.Graphics.UserInterface
|
||||||
{
|
{
|
||||||
@ -42,13 +45,13 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private GameHost host { get; set; }
|
private GameHost? host { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load()
|
private void load(OverlayColourProvider? colourProvider)
|
||||||
{
|
{
|
||||||
BackgroundUnfocused = new Color4(10, 10, 10, 255);
|
BackgroundUnfocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255);
|
||||||
BackgroundFocused = new Color4(10, 10, 10, 255);
|
BackgroundFocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We may not be focused yet, but we need to handle keyboard input to be able to request focus
|
// We may not be focused yet, but we need to handle keyboard input to be able to request focus
|
||||||
|
@ -21,44 +21,17 @@ using osuTK.Graphics;
|
|||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterface
|
namespace osu.Game.Graphics.UserInterface
|
||||||
{
|
{
|
||||||
public class OsuDropdown<T> : Dropdown<T>, IHasAccentColour
|
public class OsuDropdown<T> : Dropdown<T>
|
||||||
{
|
{
|
||||||
private const float corner_radius = 5;
|
private const float corner_radius = 5;
|
||||||
|
|
||||||
private Color4 accentColour;
|
|
||||||
|
|
||||||
public Color4 AccentColour
|
|
||||||
{
|
|
||||||
get => accentColour;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
accentColour = value;
|
|
||||||
updateAccentColour();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
|
||||||
private void load(OverlayColourProvider? colourProvider, OsuColour colours)
|
|
||||||
{
|
|
||||||
if (accentColour == default)
|
|
||||||
accentColour = colourProvider?.Light4 ?? colours.PinkDarker;
|
|
||||||
updateAccentColour();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateAccentColour()
|
|
||||||
{
|
|
||||||
if (Header is IHasAccentColour header) header.AccentColour = accentColour;
|
|
||||||
|
|
||||||
if (Menu is IHasAccentColour menu) menu.AccentColour = accentColour;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override DropdownHeader CreateHeader() => new OsuDropdownHeader();
|
protected override DropdownHeader CreateHeader() => new OsuDropdownHeader();
|
||||||
|
|
||||||
protected override DropdownMenu CreateMenu() => new OsuDropdownMenu();
|
protected override DropdownMenu CreateMenu() => new OsuDropdownMenu();
|
||||||
|
|
||||||
#region OsuDropdownMenu
|
#region OsuDropdownMenu
|
||||||
|
|
||||||
protected class OsuDropdownMenu : DropdownMenu, IHasAccentColour
|
protected class OsuDropdownMenu : DropdownMenu
|
||||||
{
|
{
|
||||||
public override bool HandleNonPositionalInput => State == MenuState.Open;
|
public override bool HandleNonPositionalInput => State == MenuState.Open;
|
||||||
|
|
||||||
@ -78,9 +51,11 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(OverlayColourProvider? colourProvider, AudioManager audio)
|
private void load(OverlayColourProvider? colourProvider, OsuColour colours, AudioManager audio)
|
||||||
{
|
{
|
||||||
BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
|
BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
|
||||||
|
HoverColour = colourProvider?.Light4 ?? colours.PinkDarker;
|
||||||
|
SelectionColour = colourProvider?.Background3 ?? colours.PinkDarker.Opacity(0.5f);
|
||||||
|
|
||||||
sampleOpen = audio.Samples.Get(@"UI/dropdown-open");
|
sampleOpen = audio.Samples.Get(@"UI/dropdown-open");
|
||||||
sampleClose = audio.Samples.Get(@"UI/dropdown-close");
|
sampleClose = audio.Samples.Get(@"UI/dropdown-close");
|
||||||
@ -121,57 +96,77 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Color4 accentColour;
|
private Color4 hoverColour;
|
||||||
|
|
||||||
public Color4 AccentColour
|
public Color4 HoverColour
|
||||||
{
|
{
|
||||||
get => accentColour;
|
get => hoverColour;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
accentColour = value;
|
hoverColour = value;
|
||||||
foreach (var c in Children.OfType<IHasAccentColour>())
|
foreach (var c in Children.OfType<DrawableOsuDropdownMenuItem>())
|
||||||
c.AccentColour = value;
|
c.BackgroundColourHover = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color4 selectionColour;
|
||||||
|
|
||||||
|
public Color4 SelectionColour
|
||||||
|
{
|
||||||
|
get => selectionColour;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
selectionColour = value;
|
||||||
|
foreach (var c in Children.OfType<DrawableOsuDropdownMenuItem>())
|
||||||
|
c.BackgroundColourSelected = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Menu CreateSubMenu() => new OsuMenu(Direction.Vertical);
|
protected override Menu CreateSubMenu() => new OsuMenu(Direction.Vertical);
|
||||||
|
|
||||||
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableOsuDropdownMenuItem(item) { AccentColour = accentColour };
|
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableOsuDropdownMenuItem(item)
|
||||||
|
{
|
||||||
|
BackgroundColourHover = HoverColour,
|
||||||
|
BackgroundColourSelected = SelectionColour
|
||||||
|
};
|
||||||
|
|
||||||
protected override ScrollContainer<Drawable> CreateScrollContainer(Direction direction) => new OsuScrollContainer(direction);
|
protected override ScrollContainer<Drawable> CreateScrollContainer(Direction direction) => new OsuScrollContainer(direction);
|
||||||
|
|
||||||
#region DrawableOsuDropdownMenuItem
|
#region DrawableOsuDropdownMenuItem
|
||||||
|
|
||||||
public class DrawableOsuDropdownMenuItem : DrawableDropdownMenuItem, IHasAccentColour
|
public class DrawableOsuDropdownMenuItem : DrawableDropdownMenuItem
|
||||||
{
|
{
|
||||||
// IsHovered is used
|
// IsHovered is used
|
||||||
public override bool HandlePositionalInput => true;
|
public override bool HandlePositionalInput => true;
|
||||||
|
|
||||||
private Color4? accentColour;
|
public new Color4 BackgroundColourHover
|
||||||
|
|
||||||
public Color4 AccentColour
|
|
||||||
{
|
{
|
||||||
get => accentColour ?? nonAccentSelectedColour;
|
get => base.BackgroundColourHover;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
accentColour = value;
|
base.BackgroundColourHover = value;
|
||||||
|
updateColours();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new Color4 BackgroundColourSelected
|
||||||
|
{
|
||||||
|
get => base.BackgroundColourSelected;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
base.BackgroundColourSelected = value;
|
||||||
updateColours();
|
updateColours();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateColours()
|
private void updateColours()
|
||||||
{
|
{
|
||||||
BackgroundColourHover = accentColour ?? nonAccentHoverColour;
|
|
||||||
BackgroundColourSelected = accentColour ?? nonAccentSelectedColour;
|
|
||||||
BackgroundColour = BackgroundColourHover.Opacity(0);
|
BackgroundColour = BackgroundColourHover.Opacity(0);
|
||||||
|
|
||||||
UpdateBackgroundColour();
|
UpdateBackgroundColour();
|
||||||
UpdateForegroundColour();
|
UpdateForegroundColour();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Color4 nonAccentHoverColour;
|
|
||||||
private Color4 nonAccentSelectedColour;
|
|
||||||
|
|
||||||
public DrawableOsuDropdownMenuItem(MenuItem item)
|
public DrawableOsuDropdownMenuItem(MenuItem item)
|
||||||
: base(item)
|
: base(item)
|
||||||
{
|
{
|
||||||
@ -182,12 +177,8 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load()
|
||||||
{
|
{
|
||||||
nonAccentHoverColour = colours.PinkDarker;
|
|
||||||
nonAccentSelectedColour = Color4.Black.Opacity(0.5f);
|
|
||||||
updateColours();
|
|
||||||
|
|
||||||
AddInternal(new HoverSounds());
|
AddInternal(new HoverSounds());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,7 +281,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public class OsuDropdownHeader : DropdownHeader, IHasAccentColour
|
public class OsuDropdownHeader : DropdownHeader
|
||||||
{
|
{
|
||||||
protected readonly SpriteText Text;
|
protected readonly SpriteText Text;
|
||||||
|
|
||||||
@ -302,18 +293,6 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
|
|
||||||
protected readonly SpriteIcon Icon;
|
protected readonly SpriteIcon Icon;
|
||||||
|
|
||||||
private Color4 accentColour;
|
|
||||||
|
|
||||||
public virtual Color4 AccentColour
|
|
||||||
{
|
|
||||||
get => accentColour;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
accentColour = value;
|
|
||||||
BackgroundColourHover = accentColour;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public OsuDropdownHeader()
|
public OsuDropdownHeader()
|
||||||
{
|
{
|
||||||
Foreground.Padding = new MarginPadding(10);
|
Foreground.Padding = new MarginPadding(10);
|
||||||
|
@ -11,13 +11,33 @@ using osu.Framework.Input.Events;
|
|||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterface
|
namespace osu.Game.Graphics.UserInterface
|
||||||
{
|
{
|
||||||
public class OsuTabDropdown<T> : OsuDropdown<T>
|
public class OsuTabDropdown<T> : OsuDropdown<T>, IHasAccentColour
|
||||||
{
|
{
|
||||||
|
private Color4 accentColour;
|
||||||
|
|
||||||
|
public Color4 AccentColour
|
||||||
|
{
|
||||||
|
get => accentColour;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
accentColour = value;
|
||||||
|
|
||||||
|
if (IsLoaded)
|
||||||
|
propagateAccentColour();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public OsuTabDropdown()
|
public OsuTabDropdown()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
propagateAccentColour();
|
||||||
|
}
|
||||||
|
|
||||||
protected override DropdownMenu CreateMenu() => new OsuTabDropdownMenu();
|
protected override DropdownMenu CreateMenu() => new OsuTabDropdownMenu();
|
||||||
|
|
||||||
protected override DropdownHeader CreateHeader() => new OsuTabDropdownHeader
|
protected override DropdownHeader CreateHeader() => new OsuTabDropdownHeader
|
||||||
@ -26,6 +46,18 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
Origin = Anchor.TopRight
|
Origin = Anchor.TopRight
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private void propagateAccentColour()
|
||||||
|
{
|
||||||
|
if (Menu is OsuDropdownMenu dropdownMenu)
|
||||||
|
{
|
||||||
|
dropdownMenu.HoverColour = accentColour;
|
||||||
|
dropdownMenu.SelectionColour = accentColour.Opacity(0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Header is OsuTabDropdownHeader tabDropdownHeader)
|
||||||
|
tabDropdownHeader.AccentColour = accentColour;
|
||||||
|
}
|
||||||
|
|
||||||
private class OsuTabDropdownMenu : OsuDropdownMenu
|
private class OsuTabDropdownMenu : OsuDropdownMenu
|
||||||
{
|
{
|
||||||
public OsuTabDropdownMenu()
|
public OsuTabDropdownMenu()
|
||||||
@ -37,7 +69,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
MaxHeight = 400;
|
MaxHeight = 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableOsuTabDropdownMenuItem(item) { AccentColour = AccentColour };
|
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableOsuTabDropdownMenuItem(item);
|
||||||
|
|
||||||
private class DrawableOsuTabDropdownMenuItem : DrawableOsuDropdownMenuItem
|
private class DrawableOsuTabDropdownMenuItem : DrawableOsuDropdownMenuItem
|
||||||
{
|
{
|
||||||
@ -49,15 +81,18 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class OsuTabDropdownHeader : OsuDropdownHeader
|
protected class OsuTabDropdownHeader : OsuDropdownHeader, IHasAccentColour
|
||||||
{
|
{
|
||||||
public override Color4 AccentColour
|
private Color4 accentColour;
|
||||||
|
|
||||||
|
public Color4 AccentColour
|
||||||
{
|
{
|
||||||
get => base.AccentColour;
|
get => accentColour;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
base.AccentColour = value;
|
accentColour = value;
|
||||||
Foreground.Colour = value;
|
BackgroundColourHover = value;
|
||||||
|
updateColour();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,15 +128,20 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
protected override bool OnHover(HoverEvent e)
|
||||||
{
|
{
|
||||||
Foreground.Colour = BackgroundColour;
|
updateColour();
|
||||||
return base.OnHover(e);
|
return base.OnHover(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnHoverLost(HoverLostEvent e)
|
protected override void OnHoverLost(HoverLostEvent e)
|
||||||
{
|
{
|
||||||
Foreground.Colour = BackgroundColourHover;
|
updateColour();
|
||||||
base.OnHoverLost(e);
|
base.OnHoverLost(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateColour()
|
||||||
|
{
|
||||||
|
Foreground.Colour = IsHovered ? BackgroundColour : BackgroundColourHover;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
@ -17,18 +19,13 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Overlays;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterface
|
namespace osu.Game.Graphics.UserInterface
|
||||||
{
|
{
|
||||||
public class OsuTextBox : BasicTextBox
|
public class OsuTextBox : BasicTextBox
|
||||||
{
|
{
|
||||||
private readonly Sample[] textAddedSamples = new Sample[4];
|
|
||||||
private Sample capsTextAddedSample;
|
|
||||||
private Sample textRemovedSample;
|
|
||||||
private Sample textCommittedSample;
|
|
||||||
private Sample caretMovedSample;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether to allow playing a different samples based on the type of character.
|
/// Whether to allow playing a different samples based on the type of character.
|
||||||
/// If set to false, the same sample will be used for all characters.
|
/// If set to false, the same sample will be used for all characters.
|
||||||
@ -42,10 +39,17 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
protected override SpriteText CreatePlaceholder() => new OsuSpriteText
|
protected override SpriteText CreatePlaceholder() => new OsuSpriteText
|
||||||
{
|
{
|
||||||
Font = OsuFont.GetFont(italics: true),
|
Font = OsuFont.GetFont(italics: true),
|
||||||
Colour = new Color4(180, 180, 180, 255),
|
|
||||||
Margin = new MarginPadding { Left = 2 },
|
Margin = new MarginPadding { Left = 2 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly Sample?[] textAddedSamples = new Sample[4];
|
||||||
|
private Sample? capsTextAddedSample;
|
||||||
|
private Sample? textRemovedSample;
|
||||||
|
private Sample? textCommittedSample;
|
||||||
|
private Sample? caretMovedSample;
|
||||||
|
|
||||||
|
private OsuCaret? caret;
|
||||||
|
|
||||||
public OsuTextBox()
|
public OsuTextBox()
|
||||||
{
|
{
|
||||||
Height = 40;
|
Height = 40;
|
||||||
@ -56,12 +60,18 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
Current.DisabledChanged += disabled => { Alpha = disabled ? 0.3f : 1; };
|
Current.DisabledChanged += disabled => { Alpha = disabled ? 0.3f : 1; };
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(OsuColour colour, AudioManager audio)
|
private void load(OverlayColourProvider? colourProvider, OsuColour colour, AudioManager audio)
|
||||||
{
|
{
|
||||||
BackgroundUnfocused = Color4.Black.Opacity(0.5f);
|
BackgroundUnfocused = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
|
||||||
BackgroundFocused = OsuColour.Gray(0.3f).Opacity(0.8f);
|
BackgroundFocused = colourProvider?.Background4 ?? OsuColour.Gray(0.3f).Opacity(0.8f);
|
||||||
BackgroundCommit = BorderColour = colour.Yellow;
|
BackgroundCommit = BorderColour = colourProvider?.Highlight1 ?? colour.Yellow;
|
||||||
|
selectionColour = colourProvider?.Background1 ?? new Color4(249, 90, 255, 255);
|
||||||
|
|
||||||
|
if (caret != null)
|
||||||
|
caret.SelectionColour = selectionColour;
|
||||||
|
|
||||||
|
Placeholder.Colour = colourProvider?.Foreground1 ?? new Color4(180, 180, 180, 255);
|
||||||
|
|
||||||
for (int i = 0; i < textAddedSamples.Length; i++)
|
for (int i = 0; i < textAddedSamples.Length; i++)
|
||||||
textAddedSamples[i] = audio.Samples.Get($@"Keyboard/key-press-{1 + i}");
|
textAddedSamples[i] = audio.Samples.Get($@"Keyboard/key-press-{1 + i}");
|
||||||
@ -72,7 +82,9 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
caretMovedSample = audio.Samples.Get(@"Keyboard/key-movement");
|
caretMovedSample = audio.Samples.Get(@"Keyboard/key-movement");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Color4 SelectionColour => new Color4(249, 90, 255, 255);
|
private Color4 selectionColour;
|
||||||
|
|
||||||
|
protected override Color4 SelectionColour => selectionColour;
|
||||||
|
|
||||||
protected override void OnUserTextAdded(string added)
|
protected override void OnUserTextAdded(string added)
|
||||||
{
|
{
|
||||||
@ -124,7 +136,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) },
|
Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) },
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override Caret CreateCaret() => new OsuCaret
|
protected override Caret CreateCaret() => caret = new OsuCaret
|
||||||
{
|
{
|
||||||
CaretWidth = CaretWidth,
|
CaretWidth = CaretWidth,
|
||||||
SelectionColour = SelectionColour,
|
SelectionColour = SelectionColour,
|
||||||
|
@ -24,6 +24,11 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString Enabled => new TranslatableString(getKey(@"enabled"), @"Enabled");
|
public static LocalisableString Enabled => new TranslatableString(getKey(@"enabled"), @"Enabled");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Disabled"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString Disabled => new TranslatableString(getKey(@"disabled"), @"Disabled");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Default"
|
/// "Default"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -29,6 +29,9 @@ namespace osu.Game.Localisation
|
|||||||
{
|
{
|
||||||
var split = lookup.Split(':');
|
var split = lookup.Split(':');
|
||||||
|
|
||||||
|
if (split.Length < 2)
|
||||||
|
return null;
|
||||||
|
|
||||||
string ns = split[0];
|
string ns = split[0];
|
||||||
string key = split[1];
|
string key = split[1];
|
||||||
|
|
||||||
|
39
osu.Game/Localisation/ToastStrings.cs
Normal file
39
osu.Game/Localisation/ToastStrings.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// 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.Localisation;
|
||||||
|
|
||||||
|
namespace osu.Game.Localisation
|
||||||
|
{
|
||||||
|
public static class ToastStrings
|
||||||
|
{
|
||||||
|
private const string prefix = @"osu.Game.Resources.Localisation.Toast";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "no key bound"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString NoKeyBound => new TranslatableString(getKey(@"no_key_bound"), @"no key bound");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Music Playback"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString MusicPlayback => new TranslatableString(getKey(@"music_playback"), @"Music Playback");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Pause track"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString PauseTrack => new TranslatableString(getKey(@"pause_track"), @"Pause track");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Play track"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString PlayTrack => new TranslatableString(getKey(@"play_track"), @"Play track");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Restart track"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString RestartTrack => new TranslatableString(getKey(@"restart_track"), @"Restart track");
|
||||||
|
|
||||||
|
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||||
|
}
|
||||||
|
}
|
@ -63,7 +63,7 @@ namespace osu.Game.Models
|
|||||||
if (IsManaged && other.IsManaged)
|
if (IsManaged && other.IsManaged)
|
||||||
return ID == other.ID;
|
return ID == other.ID;
|
||||||
|
|
||||||
if (OnlineID >= 0 && other.OnlineID >= 0)
|
if (OnlineID > 0 && other.OnlineID > 0)
|
||||||
return OnlineID == other.OnlineID;
|
return OnlineID == other.OnlineID;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash))
|
if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash))
|
||||||
|
@ -6,12 +6,14 @@ using Newtonsoft.Json;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests.Responses
|
namespace osu.Game.Online.API.Requests.Responses
|
||||||
{
|
{
|
||||||
public class APIBeatmap : BeatmapMetadata
|
public class APIBeatmap : IBeatmapInfo
|
||||||
{
|
{
|
||||||
[JsonProperty(@"id")]
|
[JsonProperty(@"id")]
|
||||||
public int OnlineBeatmapID { get; set; }
|
public int OnlineID { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"beatmapset_id")]
|
[JsonProperty(@"beatmapset_id")]
|
||||||
public int OnlineBeatmapSetID { get; set; }
|
public int OnlineBeatmapSetID { get; set; }
|
||||||
@ -19,8 +21,14 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
[JsonProperty(@"status")]
|
[JsonProperty(@"status")]
|
||||||
public BeatmapSetOnlineStatus Status { get; set; }
|
public BeatmapSetOnlineStatus Status { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("checksum")]
|
||||||
|
public string Checksum { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty(@"user_id")]
|
||||||
|
public int AuthorID { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"beatmapset")]
|
[JsonProperty(@"beatmapset")]
|
||||||
public APIBeatmapSet BeatmapSet { get; set; }
|
public APIBeatmapSet? BeatmapSet { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"playcount")]
|
[JsonProperty(@"playcount")]
|
||||||
private int playCount { get; set; }
|
private int playCount { get; set; }
|
||||||
@ -29,10 +37,10 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
private int passCount { get; set; }
|
private int passCount { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"mode_int")]
|
[JsonProperty(@"mode_int")]
|
||||||
private int ruleset { get; set; }
|
public int RulesetID { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"difficulty_rating")]
|
[JsonProperty(@"difficulty_rating")]
|
||||||
private double starDifficulty { get; set; }
|
public double StarRating { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"drain")]
|
[JsonProperty(@"drain")]
|
||||||
private float drainRate { get; set; }
|
private float drainRate { get; set; }
|
||||||
@ -46,8 +54,10 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
[JsonProperty(@"accuracy")]
|
[JsonProperty(@"accuracy")]
|
||||||
private float overallDifficulty { get; set; }
|
private float overallDifficulty { get; set; }
|
||||||
|
|
||||||
|
public double Length => lengthInSeconds * 1000;
|
||||||
|
|
||||||
[JsonProperty(@"total_length")]
|
[JsonProperty(@"total_length")]
|
||||||
private double length { get; set; }
|
private double lengthInSeconds { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"count_circles")]
|
[JsonProperty(@"count_circles")]
|
||||||
private int circleCount { get; set; }
|
private int circleCount { get; set; }
|
||||||
@ -56,10 +66,10 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
private int sliderCount { get; set; }
|
private int sliderCount { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"version")]
|
[JsonProperty(@"version")]
|
||||||
private string version { get; set; }
|
public string DifficultyName { get; set; } = string.Empty;
|
||||||
|
|
||||||
[JsonProperty(@"failtimes")]
|
[JsonProperty(@"failtimes")]
|
||||||
private BeatmapMetrics metrics { get; set; }
|
private BeatmapMetrics? metrics { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"max_combo")]
|
[JsonProperty(@"max_combo")]
|
||||||
private int? maxCombo { get; set; }
|
private int? maxCombo { get; set; }
|
||||||
@ -70,14 +80,15 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
|
|
||||||
return new BeatmapInfo
|
return new BeatmapInfo
|
||||||
{
|
{
|
||||||
Metadata = set?.Metadata ?? this,
|
Metadata = set?.Metadata ?? new BeatmapMetadata(),
|
||||||
Ruleset = rulesets.GetRuleset(ruleset),
|
Ruleset = rulesets.GetRuleset(RulesetID),
|
||||||
StarDifficulty = starDifficulty,
|
StarDifficulty = StarRating,
|
||||||
OnlineBeatmapID = OnlineBeatmapID,
|
OnlineBeatmapID = OnlineID,
|
||||||
Version = version,
|
Version = DifficultyName,
|
||||||
// this is actually an incorrect mapping (Length is calculated as drain length in lazer's import process, see BeatmapManager.calculateLength).
|
// this is actually an incorrect mapping (Length is calculated as drain length in lazer's import process, see BeatmapManager.calculateLength).
|
||||||
Length = TimeSpan.FromSeconds(length).TotalMilliseconds,
|
Length = TimeSpan.FromSeconds(Length).TotalMilliseconds,
|
||||||
Status = Status,
|
Status = Status,
|
||||||
|
MD5Hash = Checksum,
|
||||||
BeatmapSet = set,
|
BeatmapSet = set,
|
||||||
Metrics = metrics,
|
Metrics = metrics,
|
||||||
MaxCombo = maxCombo,
|
MaxCombo = maxCombo,
|
||||||
@ -97,5 +108,28 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Implementation of IBeatmapInfo
|
||||||
|
|
||||||
|
public IBeatmapMetadataInfo Metadata => (BeatmapSet as IBeatmapSetInfo)?.Metadata ?? new BeatmapMetadata();
|
||||||
|
|
||||||
|
public IBeatmapDifficultyInfo Difficulty => new BeatmapDifficulty
|
||||||
|
{
|
||||||
|
DrainRate = drainRate,
|
||||||
|
CircleSize = circleSize,
|
||||||
|
ApproachRate = approachRate,
|
||||||
|
OverallDifficulty = overallDifficulty,
|
||||||
|
};
|
||||||
|
|
||||||
|
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
|
||||||
|
|
||||||
|
public string MD5Hash => Checksum;
|
||||||
|
|
||||||
|
public IRulesetInfo Ruleset => new RulesetInfo { ID = RulesetID };
|
||||||
|
|
||||||
|
public double BPM => throw new NotImplementedException();
|
||||||
|
public string Hash => throw new NotImplementedException();
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,115 +6,133 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Users;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests.Responses
|
namespace osu.Game.Online.API.Requests.Responses
|
||||||
{
|
{
|
||||||
public class APIBeatmapSet : BeatmapMetadata // todo: this is a bit wrong...
|
public class APIBeatmapSet : IBeatmapSetOnlineInfo, IBeatmapSetInfo
|
||||||
{
|
{
|
||||||
[JsonProperty(@"covers")]
|
[JsonProperty(@"covers")]
|
||||||
private BeatmapSetOnlineCovers covers { get; set; }
|
public BeatmapSetOnlineCovers Covers { get; set; }
|
||||||
|
|
||||||
private int? onlineBeatmapSetID;
|
|
||||||
|
|
||||||
[JsonProperty(@"id")]
|
[JsonProperty(@"id")]
|
||||||
public int? OnlineBeatmapSetID
|
public int OnlineID { get; set; }
|
||||||
{
|
|
||||||
get => onlineBeatmapSetID;
|
|
||||||
set => onlineBeatmapSetID = value > 0 ? value : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonProperty(@"status")]
|
[JsonProperty(@"status")]
|
||||||
public BeatmapSetOnlineStatus Status { get; set; }
|
public BeatmapSetOnlineStatus Status { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"preview_url")]
|
[JsonProperty(@"preview_url")]
|
||||||
private string preview { get; set; }
|
public string Preview { get; set; } = string.Empty;
|
||||||
|
|
||||||
[JsonProperty(@"has_favourited")]
|
[JsonProperty(@"has_favourited")]
|
||||||
private bool hasFavourited { get; set; }
|
public bool HasFavourited { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"play_count")]
|
[JsonProperty(@"play_count")]
|
||||||
private int playCount { get; set; }
|
public int PlayCount { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"favourite_count")]
|
[JsonProperty(@"favourite_count")]
|
||||||
private int favouriteCount { get; set; }
|
public int FavouriteCount { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"bpm")]
|
[JsonProperty(@"bpm")]
|
||||||
private double bpm { get; set; }
|
public double BPM { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"nsfw")]
|
[JsonProperty(@"nsfw")]
|
||||||
private bool hasExplicitContent { get; set; }
|
public bool HasExplicitContent { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"video")]
|
[JsonProperty(@"video")]
|
||||||
private bool hasVideo { get; set; }
|
public bool HasVideo { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"storyboard")]
|
[JsonProperty(@"storyboard")]
|
||||||
private bool hasStoryboard { get; set; }
|
public bool HasStoryboard { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"submitted_date")]
|
[JsonProperty(@"submitted_date")]
|
||||||
private DateTimeOffset submitted { get; set; }
|
public DateTimeOffset Submitted { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"ranked_date")]
|
[JsonProperty(@"ranked_date")]
|
||||||
private DateTimeOffset? ranked { get; set; }
|
public DateTimeOffset? Ranked { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"last_updated")]
|
[JsonProperty(@"last_updated")]
|
||||||
private DateTimeOffset lastUpdated { get; set; }
|
public DateTimeOffset? LastUpdated { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"ratings")]
|
[JsonProperty(@"ratings")]
|
||||||
private int[] ratings { get; set; }
|
private int[] ratings { get; set; } = Array.Empty<int>();
|
||||||
|
|
||||||
[JsonProperty(@"track_id")]
|
[JsonProperty(@"track_id")]
|
||||||
private int? trackId { get; set; }
|
public int? TrackId { get; set; }
|
||||||
|
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("title_unicode")]
|
||||||
|
public string TitleUnicode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Artist { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty("artist_unicode")]
|
||||||
|
public string ArtistUnicode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public User? Author = new User();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper property to deserialize a username to <see cref="User"/>.
|
||||||
|
/// </summary>
|
||||||
[JsonProperty(@"user_id")]
|
[JsonProperty(@"user_id")]
|
||||||
private int creatorId
|
public int AuthorID
|
||||||
{
|
{
|
||||||
set => Author.Id = value;
|
get => Author?.Id ?? 1;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Author ??= new User();
|
||||||
|
Author.Id = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper property to deserialize a username to <see cref="User"/>.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty(@"creator")]
|
||||||
|
public string AuthorString
|
||||||
|
{
|
||||||
|
get => Author?.Username ?? string.Empty;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Author ??= new User();
|
||||||
|
Author.Username = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonProperty(@"availability")]
|
[JsonProperty(@"availability")]
|
||||||
private BeatmapSetOnlineAvailability availability { get; set; }
|
public BeatmapSetOnlineAvailability Availability { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"genre")]
|
[JsonProperty(@"genre")]
|
||||||
private BeatmapSetOnlineGenre genre { get; set; }
|
public BeatmapSetOnlineGenre Genre { get; set; }
|
||||||
|
|
||||||
[JsonProperty(@"language")]
|
[JsonProperty(@"language")]
|
||||||
private BeatmapSetOnlineLanguage language { get; set; }
|
public BeatmapSetOnlineLanguage Language { get; set; }
|
||||||
|
|
||||||
|
public string Source { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonProperty(@"tags")]
|
||||||
|
public string Tags { get; set; } = string.Empty;
|
||||||
|
|
||||||
[JsonProperty(@"beatmaps")]
|
[JsonProperty(@"beatmaps")]
|
||||||
private IEnumerable<APIBeatmap> beatmaps { get; set; }
|
private IEnumerable<APIBeatmap> beatmaps { get; set; } = Array.Empty<APIBeatmap>();
|
||||||
|
|
||||||
public virtual BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets)
|
public virtual BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets)
|
||||||
{
|
{
|
||||||
var beatmapSet = new BeatmapSetInfo
|
var beatmapSet = new BeatmapSetInfo
|
||||||
{
|
{
|
||||||
OnlineBeatmapSetID = OnlineBeatmapSetID,
|
OnlineBeatmapSetID = OnlineID,
|
||||||
Metadata = this,
|
Metadata = metadata,
|
||||||
Status = Status,
|
Status = Status,
|
||||||
Metrics = ratings == null ? null : new BeatmapSetMetrics { Ratings = ratings },
|
Metrics = new BeatmapSetMetrics { Ratings = ratings },
|
||||||
OnlineInfo = new BeatmapSetOnlineInfo
|
OnlineInfo = this
|
||||||
{
|
|
||||||
Covers = covers,
|
|
||||||
Preview = preview,
|
|
||||||
PlayCount = playCount,
|
|
||||||
FavouriteCount = favouriteCount,
|
|
||||||
BPM = bpm,
|
|
||||||
Status = Status,
|
|
||||||
HasExplicitContent = hasExplicitContent,
|
|
||||||
HasVideo = hasVideo,
|
|
||||||
HasStoryboard = hasStoryboard,
|
|
||||||
Submitted = submitted,
|
|
||||||
Ranked = ranked,
|
|
||||||
LastUpdated = lastUpdated,
|
|
||||||
Availability = availability,
|
|
||||||
HasFavourited = hasFavourited,
|
|
||||||
Genre = genre,
|
|
||||||
Language = language,
|
|
||||||
TrackId = trackId
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beatmapSet.Beatmaps = beatmaps?.Select(b =>
|
beatmapSet.Beatmaps = beatmaps.Select(b =>
|
||||||
{
|
{
|
||||||
var beatmap = b.ToBeatmapInfo(rulesets);
|
var beatmap = b.ToBeatmapInfo(rulesets);
|
||||||
beatmap.BeatmapSet = beatmapSet;
|
beatmap.BeatmapSet = beatmapSet;
|
||||||
@ -124,5 +142,31 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
|
|
||||||
return beatmapSet;
|
return beatmapSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BeatmapMetadata metadata => new BeatmapMetadata
|
||||||
|
{
|
||||||
|
Title = Title,
|
||||||
|
TitleUnicode = TitleUnicode,
|
||||||
|
Artist = Artist,
|
||||||
|
ArtistUnicode = ArtistUnicode,
|
||||||
|
AuthorID = AuthorID,
|
||||||
|
Author = Author,
|
||||||
|
Source = Source,
|
||||||
|
Tags = Tags,
|
||||||
|
};
|
||||||
|
|
||||||
|
#region Implementation of IBeatmapSetInfo
|
||||||
|
|
||||||
|
IEnumerable<IBeatmapInfo> IBeatmapSetInfo.Beatmaps => beatmaps;
|
||||||
|
|
||||||
|
IBeatmapMetadataInfo IBeatmapSetInfo.Metadata => metadata;
|
||||||
|
|
||||||
|
DateTimeOffset IBeatmapSetInfo.DateAdded => throw new NotImplementedException();
|
||||||
|
IEnumerable<INamedFileUsage> IBeatmapSetInfo.Files => throw new NotImplementedException();
|
||||||
|
double IBeatmapSetInfo.MaxStarDifficulty => throw new NotImplementedException();
|
||||||
|
double IBeatmapSetInfo.MaxLength => throw new NotImplementedException();
|
||||||
|
double IBeatmapSetInfo.MaxBPM => throw new NotImplementedException();
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
// 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 Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Rulesets;
|
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests.Responses
|
namespace osu.Game.Online.API.Requests.Responses
|
||||||
{
|
{
|
||||||
@ -16,17 +14,19 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
public int PlayCount { get; set; }
|
public int PlayCount { get; set; }
|
||||||
|
|
||||||
[JsonProperty("beatmap")]
|
[JsonProperty("beatmap")]
|
||||||
private BeatmapInfo beatmapInfo { get; set; }
|
private APIBeatmap beatmap { get; set; }
|
||||||
|
|
||||||
[JsonProperty]
|
public APIBeatmap BeatmapInfo
|
||||||
private APIBeatmapSet beatmapSet { get; set; }
|
|
||||||
|
|
||||||
public BeatmapInfo GetBeatmapInfo(RulesetStore rulesets)
|
|
||||||
{
|
{
|
||||||
BeatmapSetInfo setInfo = beatmapSet.ToBeatmapSet(rulesets);
|
get
|
||||||
beatmapInfo.BeatmapSet = setInfo;
|
{
|
||||||
beatmapInfo.Metadata = setInfo.Metadata;
|
// old osu-web code doesn't nest set.
|
||||||
return beatmapInfo;
|
beatmap.BeatmapSet = BeatmapSet;
|
||||||
|
return beatmap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonProperty("beatmapset")]
|
||||||
|
public APIBeatmapSet BeatmapSet { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +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 Newtonsoft.Json;
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
|
||||||
using osu.Game.Rulesets;
|
|
||||||
|
|
||||||
namespace osu.Game.Online.Rooms
|
|
||||||
{
|
|
||||||
public class APIPlaylistBeatmap : APIBeatmap
|
|
||||||
{
|
|
||||||
[JsonProperty("checksum")]
|
|
||||||
public string Checksum { get; set; }
|
|
||||||
|
|
||||||
public override BeatmapInfo ToBeatmapInfo(RulesetStore rulesets)
|
|
||||||
{
|
|
||||||
var b = base.ToBeatmapInfo(rulesets);
|
|
||||||
b.MD5Hash = Checksum;
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -59,7 +59,7 @@ namespace osu.Game.Online.Rooms
|
|||||||
|
|
||||||
protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet)
|
protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet)
|
||||||
{
|
{
|
||||||
int? beatmapId = SelectedItem.Value?.Beatmap.Value.OnlineBeatmapID;
|
int beatmapId = SelectedItem.Value?.Beatmap.Value.OnlineID ?? -1;
|
||||||
string checksum = SelectedItem.Value?.Beatmap.Value.MD5Hash;
|
string checksum = SelectedItem.Value?.Beatmap.Value.MD5Hash;
|
||||||
|
|
||||||
var matchingBeatmap = databasedSet.Beatmaps.FirstOrDefault(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum);
|
var matchingBeatmap = databasedSet.Beatmaps.FirstOrDefault(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum);
|
||||||
@ -75,10 +75,10 @@ namespace osu.Game.Online.Rooms
|
|||||||
|
|
||||||
protected override bool IsModelAvailableLocally()
|
protected override bool IsModelAvailableLocally()
|
||||||
{
|
{
|
||||||
int? beatmapId = SelectedItem.Value.Beatmap.Value.OnlineBeatmapID;
|
int onlineId = SelectedItem.Value.Beatmap.Value.OnlineID;
|
||||||
string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash;
|
string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash;
|
||||||
|
|
||||||
var beatmap = Manager.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum);
|
var beatmap = Manager.QueryBeatmap(b => b.OnlineBeatmapID == onlineId && b.MD5Hash == checksum);
|
||||||
return beatmap?.BeatmapSet.DeletePending == false;
|
return beatmap?.BeatmapSet.DeletePending == false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ using Newtonsoft.Json;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
@ -42,7 +43,7 @@ namespace osu.Game.Online.Rooms
|
|||||||
public readonly BindableList<Mod> RequiredMods = new BindableList<Mod>();
|
public readonly BindableList<Mod> RequiredMods = new BindableList<Mod>();
|
||||||
|
|
||||||
[JsonProperty("beatmap")]
|
[JsonProperty("beatmap")]
|
||||||
private APIPlaylistBeatmap apiBeatmap { get; set; }
|
private APIBeatmap apiBeatmap { get; set; }
|
||||||
|
|
||||||
private APIMod[] allowedModsBacking;
|
private APIMod[] allowedModsBacking;
|
||||||
|
|
||||||
|
@ -657,9 +657,9 @@ namespace osu.Game
|
|||||||
var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l);
|
var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l);
|
||||||
|
|
||||||
if (combinations.Count == 0)
|
if (combinations.Count == 0)
|
||||||
return "none";
|
return ToastStrings.NoKeyBound;
|
||||||
|
|
||||||
return string.Join(" or ", combinations);
|
return string.Join(" / ", combinations);
|
||||||
};
|
};
|
||||||
|
|
||||||
Container logoContainer;
|
Container logoContainer;
|
||||||
|
@ -76,7 +76,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
|||||||
private readonly BeatmapSearchFilterRow<SearchExplicit> explicitContentFilter;
|
private readonly BeatmapSearchFilterRow<SearchExplicit> explicitContentFilter;
|
||||||
|
|
||||||
private readonly Box background;
|
private readonly Box background;
|
||||||
private readonly UpdateableBeatmapSetCover beatmapCover;
|
private readonly UpdateableOnlineBeatmapSetCover beatmapCover;
|
||||||
|
|
||||||
public BeatmapListingSearchControl()
|
public BeatmapListingSearchControl()
|
||||||
{
|
{
|
||||||
@ -196,7 +196,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TopSearchBeatmapSetCover : UpdateableBeatmapSetCover
|
private class TopSearchBeatmapSetCover : UpdateableOnlineBeatmapSetCover
|
||||||
{
|
{
|
||||||
protected override bool TransformImmediately => true;
|
protected override bool TransformImmediately => true;
|
||||||
}
|
}
|
||||||
|
@ -160,7 +160,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
|
|||||||
return icons;
|
return icons;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Drawable CreateBackground() => new UpdateableBeatmapSetCover
|
protected Drawable CreateBackground() => new UpdateableOnlineBeatmapSetCover
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
BeatmapSet = SetInfo,
|
BeatmapSet = SetInfo,
|
||||||
|
@ -92,7 +92,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false)
|
if (BeatmapSet.Value?.OnlineInfo?.Availability.DownloadDisabled ?? false)
|
||||||
{
|
{
|
||||||
button.Enabled.Value = false;
|
button.Enabled.Value = false;
|
||||||
button.TooltipText = "this beatmap is currently not available for download.";
|
button.TooltipText = "this beatmap is currently not available for download.";
|
||||||
|
@ -16,8 +16,8 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
{
|
{
|
||||||
private BeatmapSetInfo beatmapSet;
|
private BeatmapSetInfo beatmapSet;
|
||||||
|
|
||||||
private bool downloadDisabled => BeatmapSet?.OnlineInfo.Availability?.DownloadDisabled ?? false;
|
private bool downloadDisabled => BeatmapSet?.OnlineInfo.Availability.DownloadDisabled ?? false;
|
||||||
private bool hasExternalLink => !string.IsNullOrEmpty(BeatmapSet?.OnlineInfo.Availability?.ExternalLink);
|
private bool hasExternalLink => !string.IsNullOrEmpty(BeatmapSet?.OnlineInfo.Availability.ExternalLink);
|
||||||
|
|
||||||
private readonly LinkFlowContainer textContainer;
|
private readonly LinkFlowContainer textContainer;
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
public readonly Details Details;
|
public readonly Details Details;
|
||||||
public readonly BeatmapPicker Picker;
|
public readonly BeatmapPicker Picker;
|
||||||
|
|
||||||
private readonly UpdateableBeatmapSetCover cover;
|
private readonly UpdateableOnlineBeatmapSetCover cover;
|
||||||
private readonly Box coverGradient;
|
private readonly Box coverGradient;
|
||||||
private readonly OsuSpriteText title, artist;
|
private readonly OsuSpriteText title, artist;
|
||||||
private readonly AuthorInfo author;
|
private readonly AuthorInfo author;
|
||||||
@ -68,7 +68,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
cover = new UpdateableBeatmapSetCover
|
cover = new UpdateableOnlineBeatmapSetCover
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Masking = true,
|
Masking = true,
|
||||||
@ -266,7 +266,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
{
|
{
|
||||||
if (BeatmapSet.Value == null) return;
|
if (BeatmapSet.Value == null) return;
|
||||||
|
|
||||||
if ((BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false) && State.Value != DownloadState.LocallyAvailable)
|
if (BeatmapSet.Value.OnlineInfo.Availability.DownloadDisabled && State.Value != DownloadState.LocallyAvailable)
|
||||||
{
|
{
|
||||||
downloadButtonsContainer.Clear();
|
downloadButtonsContainer.Clear();
|
||||||
return;
|
return;
|
||||||
|
@ -117,8 +117,8 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
{
|
{
|
||||||
source.Text = b.NewValue?.Metadata.Source ?? string.Empty;
|
source.Text = b.NewValue?.Metadata.Source ?? string.Empty;
|
||||||
tags.Text = b.NewValue?.Metadata.Tags ?? string.Empty;
|
tags.Text = b.NewValue?.Metadata.Tags ?? string.Empty;
|
||||||
genre.Text = b.NewValue?.OnlineInfo?.Genre?.Name ?? string.Empty;
|
genre.Text = b.NewValue?.OnlineInfo?.Genre.Name ?? string.Empty;
|
||||||
language.Text = b.NewValue?.OnlineInfo?.Language?.Name ?? string.Empty;
|
language.Text = b.NewValue?.OnlineInfo?.Language.Name ?? string.Empty;
|
||||||
var setHasLeaderboard = b.NewValue?.OnlineInfo?.Status > 0;
|
var setHasLeaderboard = b.NewValue?.OnlineInfo?.Status > 0;
|
||||||
successRate.Alpha = setHasLeaderboard ? 1 : 0;
|
successRate.Alpha = setHasLeaderboard ? 1 : 0;
|
||||||
notRankedPlaceholder.Alpha = setHasLeaderboard ? 0 : 1;
|
notRankedPlaceholder.Alpha = setHasLeaderboard ? 0 : 1;
|
||||||
|
@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Dashboard.Home
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Masking = true,
|
Masking = true,
|
||||||
CornerRadius = 6,
|
CornerRadius = 6,
|
||||||
Child = new UpdateableBeatmapSetCover
|
Child = new UpdateableOnlineBeatmapSetCover
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
|
@ -29,12 +29,6 @@ namespace osu.Game.Overlays.Login
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(OsuColour colours)
|
|
||||||
{
|
|
||||||
AccentColour = colours.Gray5;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected class UserDropdownMenu : OsuDropdownMenu
|
protected class UserDropdownMenu : OsuDropdownMenu
|
||||||
{
|
{
|
||||||
public UserDropdownMenu()
|
public UserDropdownMenu()
|
||||||
@ -56,6 +50,8 @@ namespace osu.Game.Overlays.Login
|
|||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
BackgroundColour = colours.Gray3;
|
BackgroundColour = colours.Gray3;
|
||||||
|
SelectionColour = colours.Gray4;
|
||||||
|
HoverColour = colours.Gray5;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableUserDropdownMenuItem(item);
|
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableUserDropdownMenuItem(item);
|
||||||
@ -118,6 +114,7 @@ namespace osu.Game.Overlays.Login
|
|||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
BackgroundColour = colours.Gray3;
|
BackgroundColour = colours.Gray3;
|
||||||
|
BackgroundColourHover = colours.Gray5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,12 +19,6 @@ namespace osu.Game.Overlays.Music
|
|||||||
{
|
{
|
||||||
protected override bool ShowManageCollectionsItem => false;
|
protected override bool ShowManageCollectionsItem => false;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(OsuColour colours)
|
|
||||||
{
|
|
||||||
AccentColour = colours.Gray6;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override CollectionDropdownHeader CreateCollectionHeader() => new CollectionsHeader();
|
protected override CollectionDropdownHeader CreateCollectionHeader() => new CollectionsHeader();
|
||||||
|
|
||||||
protected override CollectionDropdownMenu CreateCollectionMenu() => new CollectionsMenu();
|
protected override CollectionDropdownMenu CreateCollectionMenu() => new CollectionsMenu();
|
||||||
@ -41,6 +35,8 @@ namespace osu.Game.Overlays.Music
|
|||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
BackgroundColour = colours.Gray4;
|
BackgroundColour = colours.Gray4;
|
||||||
|
SelectionColour = colours.Gray5;
|
||||||
|
HoverColour = colours.Gray6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,6 +46,7 @@ namespace osu.Game.Overlays.Music
|
|||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
BackgroundColour = colours.Gray4;
|
BackgroundColour = colours.Gray4;
|
||||||
|
BackgroundColourHover = colours.Gray6;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CollectionsHeader()
|
public CollectionsHeader()
|
||||||
|
@ -3,12 +3,15 @@
|
|||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.LocalisationExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Overlays.OSD;
|
using osu.Game.Overlays.OSD;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Music
|
namespace osu.Game.Overlays.Music
|
||||||
@ -39,11 +42,11 @@ namespace osu.Game.Overlays.Music
|
|||||||
bool wasPlaying = musicController.IsPlaying;
|
bool wasPlaying = musicController.IsPlaying;
|
||||||
|
|
||||||
if (musicController.TogglePause())
|
if (musicController.TogglePause())
|
||||||
onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track", e.Action));
|
onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? ToastStrings.PauseTrack : ToastStrings.PlayTrack, e.Action));
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case GlobalAction.MusicNext:
|
case GlobalAction.MusicNext:
|
||||||
musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track", e.Action)));
|
musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast(GlobalActionKeyBindingStrings.MusicNext, e.Action)));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@ -53,11 +56,11 @@ namespace osu.Game.Overlays.Music
|
|||||||
switch (res)
|
switch (res)
|
||||||
{
|
{
|
||||||
case PreviousTrackResult.Restart:
|
case PreviousTrackResult.Restart:
|
||||||
onScreenDisplay?.Display(new MusicActionToast("Restart track", e.Action));
|
onScreenDisplay?.Display(new MusicActionToast(ToastStrings.RestartTrack, e.Action));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PreviousTrackResult.Previous:
|
case PreviousTrackResult.Previous:
|
||||||
onScreenDisplay?.Display(new MusicActionToast("Previous track", e.Action));
|
onScreenDisplay?.Display(new MusicActionToast(GlobalActionKeyBindingStrings.MusicPrev, e.Action));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -76,8 +79,8 @@ namespace osu.Game.Overlays.Music
|
|||||||
{
|
{
|
||||||
private readonly GlobalAction action;
|
private readonly GlobalAction action;
|
||||||
|
|
||||||
public MusicActionToast(string value, GlobalAction action)
|
public MusicActionToast(LocalisableString value, GlobalAction action)
|
||||||
: base("Music Playback", value, string.Empty)
|
: base(ToastStrings.MusicPlayback, value, string.Empty)
|
||||||
{
|
{
|
||||||
this.action = action;
|
this.action = action;
|
||||||
}
|
}
|
||||||
@ -85,7 +88,7 @@ namespace osu.Game.Overlays.Music
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuConfigManager config)
|
private void load(OsuConfigManager config)
|
||||||
{
|
{
|
||||||
ShortcutText.Text = config.LookupKeyBindings(action).ToUpperInvariant();
|
ShortcutText.Text = config.LookupKeyBindings(action).ToUpper();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
// 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.Extensions.LocalisationExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
using osu.Game.Localisation;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.OSD
|
namespace osu.Game.Overlays.OSD
|
||||||
{
|
{
|
||||||
@ -23,7 +26,7 @@ namespace osu.Game.Overlays.OSD
|
|||||||
|
|
||||||
protected readonly OsuSpriteText ShortcutText;
|
protected readonly OsuSpriteText ShortcutText;
|
||||||
|
|
||||||
protected Toast(string description, string value, string shortcut)
|
protected Toast(LocalisableString description, LocalisableString value, LocalisableString shortcut)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre;
|
Anchor = Anchor.Centre;
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
@ -60,12 +63,12 @@ namespace osu.Game.Overlays.OSD
|
|||||||
Spacing = new Vector2(1, 0),
|
Spacing = new Vector2(1, 0),
|
||||||
Anchor = Anchor.TopCentre,
|
Anchor = Anchor.TopCentre,
|
||||||
Origin = Anchor.TopCentre,
|
Origin = Anchor.TopCentre,
|
||||||
Text = description.ToUpperInvariant()
|
Text = description.ToUpper()
|
||||||
},
|
},
|
||||||
ValueText = new OsuSpriteText
|
ValueText = new OsuSpriteText
|
||||||
{
|
{
|
||||||
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light),
|
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light),
|
||||||
Padding = new MarginPadding { Left = 10, Right = 10 },
|
Padding = new MarginPadding { Horizontal = 10 },
|
||||||
Name = "Value",
|
Name = "Value",
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -77,9 +80,9 @@ namespace osu.Game.Overlays.OSD
|
|||||||
Origin = Anchor.BottomCentre,
|
Origin = Anchor.BottomCentre,
|
||||||
Name = "Shortcut",
|
Name = "Shortcut",
|
||||||
Alpha = 0.3f,
|
Alpha = 0.3f,
|
||||||
Margin = new MarginPadding { Bottom = 15 },
|
Margin = new MarginPadding { Bottom = 15, Horizontal = 10 },
|
||||||
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
|
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
|
||||||
Text = string.IsNullOrEmpty(shortcut) ? "NO KEY BOUND" : shortcut.ToUpperInvariant()
|
Text = string.IsNullOrEmpty(shortcut.ToString()) ? ToastStrings.NoKeyBound.ToUpper() : shortcut.ToUpper()
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Overlays.OSD
|
|||||||
private Sample sampleChange;
|
private Sample sampleChange;
|
||||||
|
|
||||||
public TrackedSettingToast(SettingDescription description)
|
public TrackedSettingToast(SettingDescription description)
|
||||||
: base(description.Name.ToString(), description.Value.ToString(), description.Shortcut.ToString())
|
: base(description.Name, description.Value, description.Shortcut)
|
||||||
{
|
{
|
||||||
FillFlowContainer<OptionLight> optionLights;
|
FillFlowContainer<OptionLight> optionLights;
|
||||||
|
|
||||||
|
@ -15,9 +15,9 @@ namespace osu.Game.Overlays.Profile.Sections
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BeatmapMetadataContainer : OsuHoverContainer
|
public abstract class BeatmapMetadataContainer : OsuHoverContainer
|
||||||
{
|
{
|
||||||
private readonly BeatmapInfo beatmapInfo;
|
private readonly IBeatmapInfo beatmapInfo;
|
||||||
|
|
||||||
protected BeatmapMetadataContainer(BeatmapInfo beatmapInfo)
|
protected BeatmapMetadataContainer(IBeatmapInfo beatmapInfo)
|
||||||
: base(HoverSampleSet.Submit)
|
: base(HoverSampleSet.Submit)
|
||||||
{
|
{
|
||||||
this.beatmapInfo = beatmapInfo;
|
this.beatmapInfo = beatmapInfo;
|
||||||
@ -30,10 +30,7 @@ namespace osu.Game.Overlays.Profile.Sections
|
|||||||
{
|
{
|
||||||
Action = () =>
|
Action = () =>
|
||||||
{
|
{
|
||||||
if (beatmapInfo.OnlineBeatmapID != null)
|
beatmapSetOverlay?.FetchAndShowBeatmap(beatmapInfo.OnlineID);
|
||||||
beatmapSetOverlay?.FetchAndShowBeatmap(beatmapInfo.OnlineBeatmapID.Value);
|
|
||||||
else if (beatmapInfo.BeatmapSet?.OnlineBeatmapSetID != null)
|
|
||||||
beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapInfo.BeatmapSet.OnlineBeatmapSetID.Value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Child = new FillFlowContainer
|
Child = new FillFlowContainer
|
||||||
@ -43,6 +40,6 @@ namespace osu.Game.Overlays.Profile.Sections
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract Drawable[] CreateText(BeatmapInfo beatmapInfo);
|
protected abstract Drawable[] CreateText(IBeatmapInfo beatmapInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,12 +60,12 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
|
|||||||
protected override APIRequest<List<APIBeatmapSet>> CreateRequest() =>
|
protected override APIRequest<List<APIBeatmapSet>> CreateRequest() =>
|
||||||
new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage);
|
new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage);
|
||||||
|
|
||||||
protected override Drawable CreateDrawableItem(APIBeatmapSet model) => !model.OnlineBeatmapSetID.HasValue
|
protected override Drawable CreateDrawableItem(APIBeatmapSet model) => model.OnlineID > 0
|
||||||
? null
|
? new GridBeatmapPanel(model.ToBeatmapSet(Rulesets))
|
||||||
: new GridBeatmapPanel(model.ToBeatmapSet(Rulesets))
|
|
||||||
{
|
{
|
||||||
Anchor = Anchor.TopCentre,
|
Anchor = Anchor.TopCentre,
|
||||||
Origin = Anchor.TopCentre,
|
Origin = Anchor.TopCentre,
|
||||||
};
|
}
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// 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.Diagnostics;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -13,6 +14,7 @@ using osu.Game.Graphics.Sprites;
|
|||||||
using osuTK;
|
using osuTK;
|
||||||
using osu.Framework.Graphics.Cursor;
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Resources.Localisation.Web;
|
using osu.Game.Resources.Localisation.Web;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Profile.Sections.Historical
|
namespace osu.Game.Overlays.Profile.Sections.Historical
|
||||||
@ -22,13 +24,11 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
|||||||
private const int cover_width = 100;
|
private const int cover_width = 100;
|
||||||
private const int corner_radius = 6;
|
private const int corner_radius = 6;
|
||||||
|
|
||||||
private readonly BeatmapInfo beatmapInfo;
|
private readonly APIUserMostPlayedBeatmap mostPlayed;
|
||||||
private readonly int playCount;
|
|
||||||
|
|
||||||
public DrawableMostPlayedBeatmap(BeatmapInfo beatmapInfo, int playCount)
|
public DrawableMostPlayedBeatmap(APIUserMostPlayedBeatmap mostPlayed)
|
||||||
{
|
{
|
||||||
this.beatmapInfo = beatmapInfo;
|
this.mostPlayed = mostPlayed;
|
||||||
this.playCount = playCount;
|
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
Height = 50;
|
Height = 50;
|
||||||
@ -42,11 +42,11 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
|||||||
{
|
{
|
||||||
AddRangeInternal(new Drawable[]
|
AddRangeInternal(new Drawable[]
|
||||||
{
|
{
|
||||||
new UpdateableBeatmapSetCover(BeatmapSetCoverType.List)
|
new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Y,
|
RelativeSizeAxes = Axes.Y,
|
||||||
Width = cover_width,
|
Width = cover_width,
|
||||||
BeatmapSet = beatmapInfo.BeatmapSet,
|
BeatmapSet = mostPlayed.BeatmapSet,
|
||||||
},
|
},
|
||||||
new Container
|
new Container
|
||||||
{
|
{
|
||||||
@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
|||||||
Direction = FillDirection.Vertical,
|
Direction = FillDirection.Vertical,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new MostPlayedBeatmapMetadataContainer(beatmapInfo),
|
new MostPlayedBeatmapMetadataContainer(mostPlayed.BeatmapInfo),
|
||||||
new LinkFlowContainer(t =>
|
new LinkFlowContainer(t =>
|
||||||
{
|
{
|
||||||
t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular);
|
t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular);
|
||||||
@ -89,11 +89,11 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
|||||||
}.With(d =>
|
}.With(d =>
|
||||||
{
|
{
|
||||||
d.AddText("mapped by ");
|
d.AddText("mapped by ");
|
||||||
d.AddUserLink(beatmapInfo.Metadata.Author);
|
d.AddUserLink(mostPlayed.BeatmapSet.Author);
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new PlayCountText(playCount)
|
new PlayCountText(mostPlayed.PlayCount)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreRight,
|
Anchor = Anchor.CentreRight,
|
||||||
Origin = Anchor.CentreRight
|
Origin = Anchor.CentreRight
|
||||||
@ -120,27 +120,42 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
|||||||
|
|
||||||
private class MostPlayedBeatmapMetadataContainer : BeatmapMetadataContainer
|
private class MostPlayedBeatmapMetadataContainer : BeatmapMetadataContainer
|
||||||
{
|
{
|
||||||
public MostPlayedBeatmapMetadataContainer(BeatmapInfo beatmapInfo)
|
public MostPlayedBeatmapMetadataContainer(IBeatmapInfo beatmapInfo)
|
||||||
: base(beatmapInfo)
|
: base(beatmapInfo)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Drawable[] CreateText(BeatmapInfo beatmapInfo) => new Drawable[]
|
protected override Drawable[] CreateText(IBeatmapInfo beatmapInfo)
|
||||||
|
{
|
||||||
|
var metadata = beatmapInfo.Metadata;
|
||||||
|
|
||||||
|
Debug.Assert(metadata != null);
|
||||||
|
|
||||||
|
return new Drawable[]
|
||||||
{
|
{
|
||||||
new OsuSpriteText
|
new OsuSpriteText
|
||||||
{
|
{
|
||||||
Text = new RomanisableString(
|
Text = new RomanisableString(metadata.TitleUnicode, metadata.Title),
|
||||||
$"{beatmapInfo.Metadata.TitleUnicode ?? beatmapInfo.Metadata.Title} [{beatmapInfo.Version}] ",
|
|
||||||
$"{beatmapInfo.Metadata.Title ?? beatmapInfo.Metadata.TitleUnicode} [{beatmapInfo.Version}] "),
|
|
||||||
Font = OsuFont.GetFont(weight: FontWeight.Bold)
|
Font = OsuFont.GetFont(weight: FontWeight.Bold)
|
||||||
},
|
},
|
||||||
new OsuSpriteText
|
new OsuSpriteText
|
||||||
{
|
{
|
||||||
Text = "by " + new RomanisableString(beatmapInfo.Metadata.ArtistUnicode, beatmapInfo.Metadata.Artist),
|
Text = $" [{beatmapInfo.DifficultyName}]",
|
||||||
|
Font = OsuFont.GetFont(weight: FontWeight.Bold)
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Text = " by ",
|
||||||
|
Font = OsuFont.GetFont(weight: FontWeight.Regular)
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist),
|
||||||
Font = OsuFont.GetFont(weight: FontWeight.Regular)
|
Font = OsuFont.GetFont(weight: FontWeight.Regular)
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class PlayCountText : CompositeDrawable, IHasTooltip
|
private class PlayCountText : CompositeDrawable, IHasTooltip
|
||||||
{
|
{
|
||||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
|||||||
protected override APIRequest<List<APIUserMostPlayedBeatmap>> CreateRequest() =>
|
protected override APIRequest<List<APIUserMostPlayedBeatmap>> CreateRequest() =>
|
||||||
new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++, ItemsPerPage);
|
new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++, ItemsPerPage);
|
||||||
|
|
||||||
protected override Drawable CreateDrawableItem(APIUserMostPlayedBeatmap model) =>
|
protected override Drawable CreateDrawableItem(APIUserMostPlayedBeatmap mostPlayed) =>
|
||||||
new DrawableMostPlayedBeatmap(model.GetBeatmapInfo(Rulesets), model.PlayCount);
|
new DrawableMostPlayedBeatmap(mostPlayed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// 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.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -245,30 +246,42 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
|
|||||||
|
|
||||||
private class ScoreBeatmapMetadataContainer : BeatmapMetadataContainer
|
private class ScoreBeatmapMetadataContainer : BeatmapMetadataContainer
|
||||||
{
|
{
|
||||||
public ScoreBeatmapMetadataContainer(BeatmapInfo beatmapInfo)
|
public ScoreBeatmapMetadataContainer(IBeatmapInfo beatmapInfo)
|
||||||
: base(beatmapInfo)
|
: base(beatmapInfo)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Drawable[] CreateText(BeatmapInfo beatmapInfo) => new Drawable[]
|
protected override Drawable[] CreateText(IBeatmapInfo beatmapInfo)
|
||||||
|
{
|
||||||
|
var metadata = beatmapInfo.Metadata;
|
||||||
|
|
||||||
|
Debug.Assert(metadata != null);
|
||||||
|
|
||||||
|
return new Drawable[]
|
||||||
{
|
{
|
||||||
new OsuSpriteText
|
new OsuSpriteText
|
||||||
{
|
{
|
||||||
Anchor = Anchor.BottomLeft,
|
Anchor = Anchor.BottomLeft,
|
||||||
Origin = Anchor.BottomLeft,
|
Origin = Anchor.BottomLeft,
|
||||||
Text = new RomanisableString(
|
Text = new RomanisableString(metadata.TitleUnicode, metadata.Title),
|
||||||
$"{beatmapInfo.Metadata.TitleUnicode ?? beatmapInfo.Metadata.Title} ",
|
|
||||||
$"{beatmapInfo.Metadata.Title ?? beatmapInfo.Metadata.TitleUnicode} "),
|
|
||||||
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold, italics: true)
|
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold, italics: true)
|
||||||
},
|
},
|
||||||
new OsuSpriteText
|
new OsuSpriteText
|
||||||
{
|
{
|
||||||
Anchor = Anchor.BottomLeft,
|
Anchor = Anchor.BottomLeft,
|
||||||
Origin = Anchor.BottomLeft,
|
Origin = Anchor.BottomLeft,
|
||||||
Text = "by " + new RomanisableString(beatmapInfo.Metadata.ArtistUnicode, beatmapInfo.Metadata.Artist),
|
Text = " by ",
|
||||||
|
Font = OsuFont.GetFont(size: 12, italics: true)
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist),
|
||||||
Font = OsuFont.GetFont(size: 12, italics: true)
|
Font = OsuFont.GetFont(size: 12, italics: true)
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -175,18 +175,18 @@ namespace osu.Game.Overlays.Rankings
|
|||||||
|
|
||||||
private class SpotlightsDropdown : OsuDropdown<APISpotlight>
|
private class SpotlightsDropdown : OsuDropdown<APISpotlight>
|
||||||
{
|
{
|
||||||
private DropdownMenu menu;
|
private OsuDropdownMenu menu;
|
||||||
|
|
||||||
protected override DropdownMenu CreateMenu() => menu = base.CreateMenu().With(m => m.MaxHeight = 400);
|
protected override DropdownMenu CreateMenu() => menu = (OsuDropdownMenu)base.CreateMenu().With(m => m.MaxHeight = 400);
|
||||||
|
|
||||||
protected override DropdownHeader CreateHeader() => new SpotlightsDropdownHeader();
|
protected override DropdownHeader CreateHeader() => new SpotlightsDropdownHeader();
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OverlayColourProvider colourProvider)
|
private void load(OverlayColourProvider colourProvider)
|
||||||
{
|
{
|
||||||
// osu-web adds a 0.6 opacity container on top of the 0.5 base one when hovering, 0.8 on a single container here matches the resulting colour
|
|
||||||
AccentColour = colourProvider.Background6.Opacity(0.8f);
|
|
||||||
menu.BackgroundColour = colourProvider.Background5;
|
menu.BackgroundColour = colourProvider.Background5;
|
||||||
|
menu.HoverColour = colourProvider.Background4;
|
||||||
|
menu.SelectionColour = colourProvider.Background3;
|
||||||
Padding = new MarginPadding { Vertical = 20 };
|
Padding = new MarginPadding { Vertical = 20 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,7 +205,8 @@ namespace osu.Game.Overlays.Rankings
|
|||||||
private void load(OverlayColourProvider colourProvider)
|
private void load(OverlayColourProvider colourProvider)
|
||||||
{
|
{
|
||||||
BackgroundColour = colourProvider.Background6.Opacity(0.5f);
|
BackgroundColour = colourProvider.Background6.Opacity(0.5f);
|
||||||
BackgroundColourHover = colourProvider.Background5;
|
// osu-web adds a 0.6 opacity container on top of the 0.5 base one when hovering, 0.8 on a single container here matches the resulting colour
|
||||||
|
BackgroundColourHover = colourProvider.Background6.Opacity(0.8f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,8 @@
|
|||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osuTK.Graphics;
|
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Colour;
|
|
||||||
using osu.Framework.Graphics.Cursor;
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Graphics.Effects;
|
using osu.Framework.Graphics.Effects;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
@ -14,6 +12,7 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Overlays
|
namespace osu.Game.Overlays
|
||||||
{
|
{
|
||||||
@ -45,30 +44,21 @@ namespace osu.Game.Overlays
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool hovering;
|
[Resolved]
|
||||||
|
private OsuColour colours { get; set; }
|
||||||
|
|
||||||
public RestoreDefaultValueButton()
|
private const float size = 4;
|
||||||
{
|
|
||||||
Height = 1;
|
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Y;
|
|
||||||
Width = SettingsPanel.CONTENT_MARGINS;
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colour)
|
private void load(OsuColour colour)
|
||||||
{
|
{
|
||||||
BackgroundColour = colour.Yellow;
|
BackgroundColour = colour.Lime1;
|
||||||
Content.Width = 0.33f;
|
Size = new Vector2(3 * size);
|
||||||
Content.CornerRadius = 3;
|
|
||||||
Content.EdgeEffect = new EdgeEffectParameters
|
Content.RelativeSizeAxes = Axes.None;
|
||||||
{
|
Content.Size = new Vector2(size);
|
||||||
Colour = BackgroundColour.Opacity(0.1f),
|
Content.CornerRadius = size / 2;
|
||||||
Type = EdgeEffectType.Glow,
|
|
||||||
Radius = 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
Padding = new MarginPadding { Vertical = 1.5f };
|
|
||||||
Alpha = 0f;
|
Alpha = 0f;
|
||||||
|
|
||||||
Action += () =>
|
Action += () =>
|
||||||
@ -81,39 +71,55 @@ namespace osu.Game.Overlays
|
|||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
updateState();
|
||||||
// avoid unnecessary transforms on first display.
|
FinishTransforms(true);
|
||||||
Alpha = currentAlpha;
|
|
||||||
Background.Colour = currentColour;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocalisableString TooltipText => "revert to default";
|
public LocalisableString TooltipText => "revert to default";
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
protected override bool OnHover(HoverEvent e)
|
||||||
{
|
{
|
||||||
hovering = true;
|
|
||||||
UpdateState();
|
UpdateState();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnHoverLost(HoverLostEvent e)
|
protected override void OnHoverLost(HoverLostEvent e)
|
||||||
{
|
{
|
||||||
hovering = false;
|
|
||||||
UpdateState();
|
UpdateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateState() => Scheduler.AddOnce(updateState);
|
public void UpdateState() => Scheduler.AddOnce(updateState);
|
||||||
|
|
||||||
private float currentAlpha => current.IsDefault ? 0f : hovering && !current.Disabled ? 1f : 0.65f;
|
private const double fade_duration = 200;
|
||||||
private ColourInfo currentColour => current.Disabled ? Color4.Gray : BackgroundColour;
|
|
||||||
|
|
||||||
private void updateState()
|
private void updateState()
|
||||||
{
|
{
|
||||||
if (current == null)
|
if (current == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.FadeTo(currentAlpha, 200, Easing.OutQuint);
|
Enabled.Value = !Current.Disabled;
|
||||||
Background.FadeColour(currentColour, 200, Easing.OutQuint);
|
|
||||||
|
if (!Current.Disabled)
|
||||||
|
{
|
||||||
|
this.FadeTo(Current.IsDefault ? 0 : 1, fade_duration, Easing.OutQuint);
|
||||||
|
Background.FadeColour(IsHovered ? colours.Lime0 : colours.Lime1, fade_duration, Easing.OutQuint);
|
||||||
|
Content.TweenEdgeEffectTo(new EdgeEffectParameters
|
||||||
|
{
|
||||||
|
Colour = (IsHovered ? colours.Lime1 : colours.Lime3).Opacity(0.4f),
|
||||||
|
Radius = IsHovered ? 8 : 4,
|
||||||
|
Type = EdgeEffectType.Glow
|
||||||
|
}, fade_duration, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Background.FadeColour(colours.Lime3, fade_duration, Easing.OutQuint);
|
||||||
|
Content.TweenEdgeEffectTo(new EdgeEffectParameters
|
||||||
|
{
|
||||||
|
Colour = colours.Lime3.Opacity(0.1f),
|
||||||
|
Radius = 2,
|
||||||
|
Type = EdgeEffectType.Glow
|
||||||
|
}, fade_duration, Easing.OutQuint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,16 +82,29 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
AutoSizeAxes = Axes.Y;
|
AutoSizeAxes = Axes.Y;
|
||||||
Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS };
|
Padding = new MarginPadding { Right = SettingsPanel.CONTENT_MARGINS };
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
new RestoreDefaultValueButton<bool>
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Width = SettingsPanel.CONTENT_MARGINS,
|
||||||
|
Child = new RestoreDefaultValueButton<bool>
|
||||||
{
|
{
|
||||||
Current = isDefault,
|
Current = isDefault,
|
||||||
Action = RestoreDefaults,
|
Action = RestoreDefaults,
|
||||||
Origin = Anchor.TopRight,
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
content = new Container
|
content = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
@ -138,6 +151,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
new HoverClickSounds()
|
new HoverClickSounds()
|
||||||
};
|
};
|
||||||
|
@ -6,7 +6,6 @@ using System.Linq;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Settings
|
namespace osu.Game.Overlays.Settings
|
||||||
{
|
{
|
||||||
@ -28,11 +27,6 @@ namespace osu.Game.Overlays.Settings
|
|||||||
|
|
||||||
public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.ToString()));
|
public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.ToString()));
|
||||||
|
|
||||||
public SettingsDropdown()
|
|
||||||
{
|
|
||||||
FlowContent.Spacing = new Vector2(0, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected sealed override Drawable CreateControl() => CreateDropdown();
|
protected sealed override Drawable CreateControl() => CreateDropdown();
|
||||||
|
|
||||||
protected virtual OsuDropdown<T> CreateDropdown() => new DropdownControl();
|
protected virtual OsuDropdown<T> CreateDropdown() => new DropdownControl();
|
||||||
|
@ -14,6 +14,7 @@ using osu.Framework.Localisation;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Settings
|
namespace osu.Game.Overlays.Settings
|
||||||
{
|
{
|
||||||
@ -34,6 +35,7 @@ namespace osu.Game.Overlays.Settings
|
|||||||
private OsuTextFlowContainer warningText;
|
private OsuTextFlowContainer warningText;
|
||||||
|
|
||||||
public bool ShowsDefaultIndicator = true;
|
public bool ShowsDefaultIndicator = true;
|
||||||
|
private readonly Container defaultValueIndicatorContainer;
|
||||||
|
|
||||||
public LocalisableString TooltipText { get; set; }
|
public LocalisableString TooltipText { get; set; }
|
||||||
|
|
||||||
@ -54,6 +56,7 @@ namespace osu.Game.Overlays.Settings
|
|||||||
}
|
}
|
||||||
|
|
||||||
labelText.Text = value;
|
labelText.Text = value;
|
||||||
|
updateLayout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,16 +111,23 @@ namespace osu.Game.Overlays.Settings
|
|||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
FlowContent = new FillFlowContainer
|
defaultValueIndicatorContainer = new Container
|
||||||
|
{
|
||||||
|
Width = SettingsPanel.CONTENT_MARGINS,
|
||||||
|
},
|
||||||
|
new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },
|
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },
|
||||||
Children = new[]
|
Child = FlowContent = new FillFlowContainer
|
||||||
{
|
{
|
||||||
Control = CreateControl(),
|
RelativeSizeAxes = Axes.X,
|
||||||
},
|
AutoSizeAxes = Axes.Y,
|
||||||
},
|
Spacing = new Vector2(0, 10),
|
||||||
|
Child = Control = CreateControl(),
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// IMPORTANT: all bindable logic is in constructor intentionally to support "CreateSettingsControls" being used in a context it is
|
// IMPORTANT: all bindable logic is in constructor intentionally to support "CreateSettingsControls" being used in a context it is
|
||||||
@ -135,13 +145,25 @@ namespace osu.Game.Overlays.Settings
|
|||||||
// intentionally done before LoadComplete to avoid overhead.
|
// intentionally done before LoadComplete to avoid overhead.
|
||||||
if (ShowsDefaultIndicator)
|
if (ShowsDefaultIndicator)
|
||||||
{
|
{
|
||||||
AddInternal(new RestoreDefaultValueButton<T>
|
defaultValueIndicatorContainer.Add(new RestoreDefaultValueButton<T>
|
||||||
{
|
{
|
||||||
Current = controlWithCurrent.Current,
|
Current = controlWithCurrent.Current,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre
|
||||||
});
|
});
|
||||||
|
updateLayout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateLayout()
|
||||||
|
{
|
||||||
|
bool hasLabel = labelText != null && !string.IsNullOrEmpty(labelText.Text.ToString());
|
||||||
|
|
||||||
|
// if the settings item is providing a label, the default value indicator should be centred vertically to the left of the label.
|
||||||
|
// otherwise, it should be centred vertically to the left of the main control of the settings item.
|
||||||
|
defaultValueIndicatorContainer.Height = hasLabel ? labelText.DrawHeight : Control.DrawHeight;
|
||||||
|
}
|
||||||
|
|
||||||
private void updateDisabled()
|
private void updateDisabled()
|
||||||
{
|
{
|
||||||
if (labelText != null)
|
if (labelText != null)
|
||||||
|
@ -13,7 +13,6 @@ namespace osu.Game.Overlays.Settings
|
|||||||
protected override Drawable CreateControl() => new NumberControl
|
protected override Drawable CreateControl() => new NumberControl
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Margin = new MarginPadding { Top = 5 }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private sealed class NumberControl : CompositeDrawable, IHasCurrentValue<int?>
|
private sealed class NumberControl : CompositeDrawable, IHasCurrentValue<int?>
|
||||||
|
@ -19,7 +19,6 @@ namespace osu.Game.Overlays.Settings
|
|||||||
{
|
{
|
||||||
protected override Drawable CreateControl() => new TSlider
|
protected override Drawable CreateControl() => new TSlider
|
||||||
{
|
{
|
||||||
Margin = new MarginPadding { Vertical = 10 },
|
|
||||||
RelativeSizeAxes = Axes.X
|
RelativeSizeAxes = Axes.X
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ namespace osu.Game.Overlays.Settings
|
|||||||
{
|
{
|
||||||
protected override Drawable CreateControl() => new OutlinedTextBox
|
protected override Drawable CreateControl() => new OutlinedTextBox
|
||||||
{
|
{
|
||||||
Margin = new MarginPadding { Top = 5 },
|
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
CommitOnFocusLost = true
|
CommitOnFocusLost = true
|
||||||
};
|
};
|
||||||
|
@ -25,7 +25,7 @@ using osu.Game.Rulesets.Scoring;
|
|||||||
|
|
||||||
namespace osu.Game.Scoring
|
namespace osu.Game.Scoring
|
||||||
{
|
{
|
||||||
public class ScoreManager : IModelManager<ScoreInfo>, IModelFileManager<ScoreInfo, ScoreFileInfo>, IModelDownloader<ScoreInfo>, ICanAcceptFiles
|
public class ScoreManager : IModelManager<ScoreInfo>, IModelFileManager<ScoreInfo, ScoreFileInfo>, IModelDownloader<ScoreInfo>
|
||||||
{
|
{
|
||||||
private readonly Scheduler scheduler;
|
private readonly Scheduler scheduler;
|
||||||
private readonly Func<BeatmapDifficultyCache> difficulties;
|
private readonly Func<BeatmapDifficultyCache> difficulties;
|
||||||
|
@ -86,7 +86,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
|||||||
Text = new RomanisableString(beatmap.Value.Metadata.TitleUnicode, beatmap.Value.Metadata.Title),
|
Text = new RomanisableString(beatmap.Value.Metadata.TitleUnicode, beatmap.Value.Metadata.Title),
|
||||||
Font = OsuFont.GetFont(size: TextSize),
|
Font = OsuFont.GetFont(size: TextSize),
|
||||||
}
|
}
|
||||||
}, LinkAction.OpenBeatmap, beatmap.Value.OnlineBeatmapID.ToString(), "Open beatmap");
|
}, LinkAction.OpenBeatmap, beatmap.Value.OnlineID.ToString(), "Open beatmap");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.OnlinePlay.Components
|
|
||||||
{
|
|
||||||
public class BeatmapTypeInfo : OnlinePlayComposite
|
|
||||||
{
|
|
||||||
private LinkFlowContainer beatmapAuthor;
|
|
||||||
|
|
||||||
public BeatmapTypeInfo()
|
|
||||||
{
|
|
||||||
AutoSizeAxes = Axes.Both;
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load()
|
|
||||||
{
|
|
||||||
InternalChild = new FillFlowContainer
|
|
||||||
{
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Direction = FillDirection.Horizontal,
|
|
||||||
LayoutDuration = 100,
|
|
||||||
Spacing = new Vector2(5, 0),
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new ModeTypeInfo(),
|
|
||||||
new Container
|
|
||||||
{
|
|
||||||
AutoSizeAxes = Axes.X,
|
|
||||||
Height = 30,
|
|
||||||
Margin = new MarginPadding { Left = 5 },
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new BeatmapTitle(),
|
|
||||||
beatmapAuthor = new LinkFlowContainer(s => s.Font = s.Font.With(size: 14))
|
|
||||||
{
|
|
||||||
Anchor = Anchor.BottomLeft,
|
|
||||||
Origin = Anchor.BottomLeft,
|
|
||||||
AutoSizeAxes = Axes.Both
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Playlist.CollectionChanged += (_, __) => updateInfo();
|
|
||||||
|
|
||||||
updateInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateInfo()
|
|
||||||
{
|
|
||||||
beatmapAuthor.Clear();
|
|
||||||
|
|
||||||
var beatmap = Playlist.FirstOrDefault()?.Beatmap;
|
|
||||||
|
|
||||||
if (beatmap != null)
|
|
||||||
{
|
|
||||||
beatmapAuthor.AddText("mapped by ", s => s.Colour = OsuColour.Gray(0.8f));
|
|
||||||
beatmapAuthor.AddUserLink(beatmap.Value.Metadata.Author);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -61,7 +61,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
|||||||
{
|
{
|
||||||
var beatmap = playlistItem?.Beatmap.Value;
|
var beatmap = playlistItem?.Beatmap.Value;
|
||||||
|
|
||||||
if (background?.BeatmapInfo?.BeatmapSet?.OnlineInfo?.Covers?.Cover == beatmap?.BeatmapSet?.OnlineInfo?.Covers?.Cover)
|
if (background?.BeatmapInfo?.BeatmapSet?.OnlineInfo?.Covers.Cover == beatmap?.BeatmapSet?.OnlineInfo?.Covers.Cover)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
cancellationSource?.Cancel();
|
cancellationSource?.Cancel();
|
||||||
|
@ -17,7 +17,6 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Input;
|
using osu.Game.Input;
|
||||||
@ -126,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = Header.HEIGHT,
|
Height = Header.HEIGHT,
|
||||||
Child = searchTextBox = new LoungeSearchTextBox
|
Child = searchTextBox = new SearchTextBox
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreRight,
|
Anchor = Anchor.CentreRight,
|
||||||
Origin = Anchor.CentreRight,
|
Origin = Anchor.CentreRight,
|
||||||
@ -362,15 +361,5 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
|
|||||||
protected abstract RoomSubScreen CreateRoomSubScreen(Room room);
|
protected abstract RoomSubScreen CreateRoomSubScreen(Room room);
|
||||||
|
|
||||||
protected abstract ListingPollingComponent CreatePollingComponent();
|
protected abstract ListingPollingComponent CreatePollingComponent();
|
||||||
|
|
||||||
private class LoungeSearchTextBox : SearchTextBox
|
|
||||||
{
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load()
|
|
||||||
{
|
|
||||||
BackgroundUnfocused = OsuColour.Gray(0.06f);
|
|
||||||
BackgroundFocused = OsuColour.Gray(0.12f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,6 @@ using osu.Game.Graphics.UserInterface;
|
|||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.OnlinePlay.Match.Components
|
namespace osu.Game.Screens.OnlinePlay.Match.Components
|
||||||
{
|
{
|
||||||
@ -91,31 +90,6 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class SettingsTextBox : OsuTextBox
|
|
||||||
{
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load()
|
|
||||||
{
|
|
||||||
BackgroundUnfocused = Color4.Black;
|
|
||||||
BackgroundFocused = Color4.Black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected class SettingsNumberTextBox : SettingsTextBox
|
|
||||||
{
|
|
||||||
protected override bool CanAddCharacter(char character) => char.IsNumber(character);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected class SettingsPasswordTextBox : OsuPasswordTextBox
|
|
||||||
{
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load()
|
|
||||||
{
|
|
||||||
BackgroundUnfocused = Color4.Black;
|
|
||||||
BackgroundFocused = Color4.Black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected class SectionContainer : FillFlowContainer<Section>
|
protected class SectionContainer : FillFlowContainer<Section>
|
||||||
{
|
{
|
||||||
public SectionContainer()
|
public SectionContainer()
|
||||||
|
@ -153,7 +153,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
{
|
{
|
||||||
new Section("Room name")
|
new Section("Room name")
|
||||||
{
|
{
|
||||||
Child = NameField = new SettingsTextBox
|
Child = NameField = new OsuTextBox
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
TabbableContentContainer = this,
|
TabbableContentContainer = this,
|
||||||
@ -202,7 +202,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
new Section("Max participants")
|
new Section("Max participants")
|
||||||
{
|
{
|
||||||
Alpha = disabled_alpha,
|
Alpha = disabled_alpha,
|
||||||
Child = MaxParticipantsField = new SettingsNumberTextBox
|
Child = MaxParticipantsField = new OsuNumberBox
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
TabbableContentContainer = this,
|
TabbableContentContainer = this,
|
||||||
@ -211,7 +211,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
},
|
},
|
||||||
new Section("Password (optional)")
|
new Section("Password (optional)")
|
||||||
{
|
{
|
||||||
Child = PasswordTextBox = new SettingsPasswordTextBox
|
Child = PasswordTextBox = new OsuPasswordTextBox
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
TabbableContentContainer = this,
|
TabbableContentContainer = this,
|
||||||
|
@ -121,7 +121,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
{
|
{
|
||||||
new Section("Room name")
|
new Section("Room name")
|
||||||
{
|
{
|
||||||
Child = NameField = new SettingsTextBox
|
Child = NameField = new OsuTextBox
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
TabbableContentContainer = this,
|
TabbableContentContainer = this,
|
||||||
@ -150,7 +150,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
},
|
},
|
||||||
new Section("Allowed attempts (across all playlist items)")
|
new Section("Allowed attempts (across all playlist items)")
|
||||||
{
|
{
|
||||||
Child = MaxAttemptsField = new SettingsNumberTextBox
|
Child = MaxAttemptsField = new OsuNumberBox
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
TabbableContentContainer = this,
|
TabbableContentContainer = this,
|
||||||
@ -168,7 +168,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
new Section("Max participants")
|
new Section("Max participants")
|
||||||
{
|
{
|
||||||
Alpha = disabled_alpha,
|
Alpha = disabled_alpha,
|
||||||
Child = MaxParticipantsField = new SettingsNumberTextBox
|
Child = MaxParticipantsField = new OsuNumberBox
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
TabbableContentContainer = this,
|
TabbableContentContainer = this,
|
||||||
@ -178,7 +178,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
new Section("Password (optional)")
|
new Section("Password (optional)")
|
||||||
{
|
{
|
||||||
Alpha = disabled_alpha,
|
Alpha = disabled_alpha,
|
||||||
Child = new SettingsPasswordTextBox
|
Child = new OsuPasswordTextBox
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
TabbableContentContainer = this,
|
TabbableContentContainer = this,
|
||||||
|
@ -106,6 +106,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ =>
|
this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ =>
|
||||||
{
|
{
|
||||||
|
RemoveFilters();
|
||||||
OnComplete?.Invoke();
|
OnComplete?.Invoke();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -137,6 +138,9 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
public void RemoveFilters()
|
public void RemoveFilters()
|
||||||
{
|
{
|
||||||
|
if (filters.Parent == null)
|
||||||
|
return;
|
||||||
|
|
||||||
RemoveInternal(filters);
|
RemoveInternal(filters);
|
||||||
filters.Dispose();
|
filters.Dispose();
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
|||||||
|
|
||||||
protected override Drawable CreateControl() => new Sliderbar
|
protected override Drawable CreateControl() => new Sliderbar
|
||||||
{
|
{
|
||||||
Margin = new MarginPadding { Top = 5, Bottom = 5 },
|
|
||||||
RelativeSizeAxes = Axes.X
|
RelativeSizeAxes = Axes.X
|
||||||
};
|
};
|
||||||
|
|
||||||
|
331
osu.Game/Stores/BeatmapImporter.cs
Normal file
331
osu.Game/Stores/BeatmapImporter.cs
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
// 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.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NuGet.Packaging;
|
||||||
|
using osu.Framework.Audio.Track;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.Formats;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.IO;
|
||||||
|
using osu.Game.IO.Archives;
|
||||||
|
using osu.Game.Models;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Stores
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
|
||||||
|
/// </summary>
|
||||||
|
[ExcludeFromDynamicCompile]
|
||||||
|
public class BeatmapImporter : RealmArchiveModelImporter<RealmBeatmapSet>, IDisposable
|
||||||
|
{
|
||||||
|
public override IEnumerable<string> HandledExtensions => new[] { ".osz" };
|
||||||
|
|
||||||
|
protected override string[] HashableFileTypes => new[] { ".osu" };
|
||||||
|
|
||||||
|
// protected override bool CheckLocalAvailability(RealmBeatmapSet model, System.Linq.IQueryable<RealmBeatmapSet> items)
|
||||||
|
// => base.CheckLocalAvailability(model, items) || (model.OnlineID > -1));
|
||||||
|
|
||||||
|
private readonly BeatmapOnlineLookupQueue? onlineLookupQueue;
|
||||||
|
|
||||||
|
public BeatmapImporter(RealmContextFactory contextFactory, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null)
|
||||||
|
: base(storage, contextFactory)
|
||||||
|
{
|
||||||
|
this.onlineLookupQueue = onlineLookupQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz";
|
||||||
|
|
||||||
|
protected override Task Populate(RealmBeatmapSet beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (archive != null)
|
||||||
|
beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet.Files, realm));
|
||||||
|
|
||||||
|
foreach (RealmBeatmap b in beatmapSet.Beatmaps)
|
||||||
|
b.BeatmapSet = beatmapSet;
|
||||||
|
|
||||||
|
validateOnlineIds(beatmapSet, realm);
|
||||||
|
|
||||||
|
bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineID > 0);
|
||||||
|
|
||||||
|
if (onlineLookupQueue != null)
|
||||||
|
{
|
||||||
|
// TODO: this required `BeatmapOnlineLookupQueue` to somehow support new types.
|
||||||
|
// await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
|
||||||
|
if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineID > 0))
|
||||||
|
{
|
||||||
|
if (beatmapSet.OnlineID > 0)
|
||||||
|
{
|
||||||
|
beatmapSet.OnlineID = -1;
|
||||||
|
LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PreImport(RealmBeatmapSet beatmapSet, Realm realm)
|
||||||
|
{
|
||||||
|
// We are about to import a new beatmap. Before doing so, ensure that no other set shares the online IDs used by the new one.
|
||||||
|
// Note that this means if the previous beatmap is restored by the user, it will no longer be linked to its online IDs.
|
||||||
|
// If this is ever an issue, we can consider marking as pending delete but not resetting the IDs (but care will be required for
|
||||||
|
// beatmaps, which don't have their own `DeletePending` state).
|
||||||
|
|
||||||
|
if (beatmapSet.OnlineID > 0)
|
||||||
|
{
|
||||||
|
var existingSetWithSameOnlineID = realm.All<RealmBeatmapSet>().SingleOrDefault(b => b.OnlineID == beatmapSet.OnlineID);
|
||||||
|
|
||||||
|
if (existingSetWithSameOnlineID != null)
|
||||||
|
{
|
||||||
|
existingSetWithSameOnlineID.DeletePending = true;
|
||||||
|
existingSetWithSameOnlineID.OnlineID = -1;
|
||||||
|
|
||||||
|
foreach (var b in existingSetWithSameOnlineID.Beatmaps)
|
||||||
|
b.OnlineID = -1;
|
||||||
|
|
||||||
|
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be deleted.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateOnlineIds(RealmBeatmapSet beatmapSet, Realm realm)
|
||||||
|
{
|
||||||
|
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineID > 0).Select(b => b.OnlineID).ToList();
|
||||||
|
|
||||||
|
// ensure all IDs are unique
|
||||||
|
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
|
||||||
|
{
|
||||||
|
LogForModel(beatmapSet, "Found non-unique IDs, resetting...");
|
||||||
|
resetIds();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find any existing beatmaps in the database that have matching online ids
|
||||||
|
List<RealmBeatmap> existingBeatmaps = new List<RealmBeatmap>();
|
||||||
|
|
||||||
|
foreach (var id in beatmapIds)
|
||||||
|
existingBeatmaps.AddRange(realm.All<RealmBeatmap>().Where(b => b.OnlineID == id));
|
||||||
|
|
||||||
|
if (existingBeatmaps.Any())
|
||||||
|
{
|
||||||
|
// reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set.
|
||||||
|
// we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted.
|
||||||
|
|
||||||
|
var existing = CheckForExisting(beatmapSet, realm);
|
||||||
|
|
||||||
|
if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b)))
|
||||||
|
{
|
||||||
|
LogForModel(beatmapSet, "Found existing import with online IDs already, resetting...");
|
||||||
|
resetIds();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineID = -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool CanSkipImport(RealmBeatmapSet existing, RealmBeatmapSet import)
|
||||||
|
{
|
||||||
|
if (!base.CanSkipImport(existing, import))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return existing.Beatmaps.Any(b => b.OnlineID > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool CanReuseExisting(RealmBeatmapSet existing, RealmBeatmapSet import)
|
||||||
|
{
|
||||||
|
if (!base.CanReuseExisting(existing, import))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var existingIds = existing.Beatmaps.Select(b => b.OnlineID).OrderBy(i => i);
|
||||||
|
var importIds = import.Beatmaps.Select(b => b.OnlineID).OrderBy(i => i);
|
||||||
|
|
||||||
|
// force re-import if we are not in a sane state.
|
||||||
|
return existing.OnlineID == import.OnlineID && existingIds.SequenceEqual(importIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string HumanisedModelName => "beatmap";
|
||||||
|
|
||||||
|
protected override RealmBeatmapSet? CreateModel(ArchiveReader reader)
|
||||||
|
{
|
||||||
|
// let's make sure there are actually .osu files to import.
|
||||||
|
string? mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(mapName))
|
||||||
|
{
|
||||||
|
Logger.Log($"No beatmap files found in the beatmap archive ({reader.Name}).", LoggingTarget.Database);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Beatmap beatmap;
|
||||||
|
using (var stream = new LineBufferedReader(reader.GetStream(mapName)))
|
||||||
|
beatmap = Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
|
||||||
|
|
||||||
|
return new RealmBeatmapSet
|
||||||
|
{
|
||||||
|
OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID ?? -1,
|
||||||
|
// Metadata = beatmap.Metadata,
|
||||||
|
DateAdded = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create all required <see cref="RealmBeatmap"/>s for the provided archive.
|
||||||
|
/// </summary>
|
||||||
|
private List<RealmBeatmap> createBeatmapDifficulties(IList<RealmNamedFileUsage> files, Realm realm)
|
||||||
|
{
|
||||||
|
var beatmaps = new List<RealmBeatmap>();
|
||||||
|
|
||||||
|
foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
using (var memoryStream = new MemoryStream(Files.Store.Get(file.File.StoragePath))) // we need a memory stream so we can seek
|
||||||
|
{
|
||||||
|
IBeatmap decoded;
|
||||||
|
using (var lineReader = new LineBufferedReader(memoryStream, true))
|
||||||
|
decoded = Decoder.GetDecoder<Beatmap>(lineReader).Decode(lineReader);
|
||||||
|
|
||||||
|
string hash = memoryStream.ComputeSHA2Hash();
|
||||||
|
|
||||||
|
if (beatmaps.Any(b => b.Hash == hash))
|
||||||
|
{
|
||||||
|
Logger.Log($"Skipping import of {file.Filename} due to duplicate file content.", LoggingTarget.Database);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var decodedInfo = decoded.BeatmapInfo;
|
||||||
|
var decodedDifficulty = decodedInfo.BaseDifficulty;
|
||||||
|
|
||||||
|
var ruleset = realm.All<RealmRuleset>().FirstOrDefault(r => r.OnlineID == decodedInfo.RulesetID);
|
||||||
|
|
||||||
|
if (ruleset?.Available != true)
|
||||||
|
{
|
||||||
|
Logger.Log($"Skipping import of {file.Filename} due to missing local ruleset {decodedInfo.RulesetID}.", LoggingTarget.Database);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var difficulty = new RealmBeatmapDifficulty
|
||||||
|
{
|
||||||
|
DrainRate = decodedDifficulty.DrainRate,
|
||||||
|
CircleSize = decodedDifficulty.CircleSize,
|
||||||
|
OverallDifficulty = decodedDifficulty.OverallDifficulty,
|
||||||
|
ApproachRate = decodedDifficulty.ApproachRate,
|
||||||
|
SliderMultiplier = decodedDifficulty.SliderMultiplier,
|
||||||
|
SliderTickRate = decodedDifficulty.SliderTickRate,
|
||||||
|
};
|
||||||
|
|
||||||
|
var metadata = new RealmBeatmapMetadata
|
||||||
|
{
|
||||||
|
Title = decoded.Metadata.Title,
|
||||||
|
TitleUnicode = decoded.Metadata.TitleUnicode,
|
||||||
|
Artist = decoded.Metadata.Artist,
|
||||||
|
ArtistUnicode = decoded.Metadata.ArtistUnicode,
|
||||||
|
Author = decoded.Metadata.AuthorString,
|
||||||
|
Source = decoded.Metadata.Source,
|
||||||
|
Tags = decoded.Metadata.Tags,
|
||||||
|
PreviewTime = decoded.Metadata.PreviewTime,
|
||||||
|
AudioFile = decoded.Metadata.AudioFile,
|
||||||
|
BackgroundFile = decoded.Metadata.BackgroundFile,
|
||||||
|
};
|
||||||
|
|
||||||
|
var beatmap = new RealmBeatmap(ruleset, difficulty, metadata)
|
||||||
|
{
|
||||||
|
Hash = hash,
|
||||||
|
DifficultyName = decodedInfo.Version,
|
||||||
|
OnlineID = decodedInfo.OnlineBeatmapID ?? -1,
|
||||||
|
AudioLeadIn = decodedInfo.AudioLeadIn,
|
||||||
|
StackLeniency = decodedInfo.StackLeniency,
|
||||||
|
SpecialStyle = decodedInfo.SpecialStyle,
|
||||||
|
LetterboxInBreaks = decodedInfo.LetterboxInBreaks,
|
||||||
|
WidescreenStoryboard = decodedInfo.WidescreenStoryboard,
|
||||||
|
EpilepsyWarning = decodedInfo.EpilepsyWarning,
|
||||||
|
SamplesMatchPlaybackRate = decodedInfo.SamplesMatchPlaybackRate,
|
||||||
|
DistanceSpacing = decodedInfo.DistanceSpacing,
|
||||||
|
BeatDivisor = decodedInfo.BeatDivisor,
|
||||||
|
GridSize = decodedInfo.GridSize,
|
||||||
|
TimelineZoom = decodedInfo.TimelineZoom,
|
||||||
|
MD5Hash = memoryStream.ComputeMD5Hash(),
|
||||||
|
};
|
||||||
|
|
||||||
|
updateBeatmapStatistics(beatmap, decoded);
|
||||||
|
|
||||||
|
beatmaps.Add(beatmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return beatmaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateBeatmapStatistics(RealmBeatmap beatmap, IBeatmap decoded)
|
||||||
|
{
|
||||||
|
var rulesetInstance = ((IRulesetInfo)beatmap.Ruleset).CreateInstance();
|
||||||
|
|
||||||
|
if (rulesetInstance == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
decoded.BeatmapInfo.Ruleset = rulesetInstance.RulesetInfo;
|
||||||
|
|
||||||
|
// TODO: this should be done in a better place once we actually need to dynamically update it.
|
||||||
|
beatmap.StarRating = rulesetInstance.CreateDifficultyCalculator(new DummyConversionBeatmap(decoded)).Calculate().StarRating;
|
||||||
|
beatmap.Length = calculateLength(decoded);
|
||||||
|
beatmap.BPM = 60000 / decoded.GetMostCommonBeatLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculateLength(IBeatmap b)
|
||||||
|
{
|
||||||
|
if (!b.HitObjects.Any())
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var lastObject = b.HitObjects.Last();
|
||||||
|
|
||||||
|
//TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list).
|
||||||
|
double endTime = lastObject.GetEndTime();
|
||||||
|
double startTime = b.HitObjects.First().StartTime;
|
||||||
|
|
||||||
|
return endTime - startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
onlineLookupQueue?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
|
||||||
|
/// </summary>
|
||||||
|
private class DummyConversionBeatmap : WorkingBeatmap
|
||||||
|
{
|
||||||
|
private readonly IBeatmap beatmap;
|
||||||
|
|
||||||
|
public DummyConversionBeatmap(IBeatmap beatmap)
|
||||||
|
: base(beatmap.BeatmapInfo, null)
|
||||||
|
{
|
||||||
|
this.beatmap = beatmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IBeatmap GetBeatmap() => beatmap;
|
||||||
|
protected override Texture? GetBackground() => null;
|
||||||
|
protected override Track? GetBeatmapTrack() => null;
|
||||||
|
protected internal override ISkin? GetSkin() => null;
|
||||||
|
public override Stream? GetStream(string storagePath) => null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
550
osu.Game/Stores/RealmArchiveModelImporter.cs
Normal file
550
osu.Game/Stores/RealmArchiveModelImporter.cs
Normal file
@ -0,0 +1,550 @@
|
|||||||
|
// 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.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Humanizer;
|
||||||
|
using NuGet.Packaging;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Threading;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.IO.Archives;
|
||||||
|
using osu.Game.Models;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Stores
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Encapsulates a model store class to give it import functionality.
|
||||||
|
/// Adds cross-functionality with <see cref="RealmFileStore"/> to give access to the central file store for the provided model.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TModel">The model type.</typeparam>
|
||||||
|
public abstract class RealmArchiveModelImporter<TModel> : IModelImporter<TModel>
|
||||||
|
where TModel : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey, ISoftDelete
|
||||||
|
{
|
||||||
|
private const int import_queue_request_concurrency = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The size of a batch import operation before considering it a lower priority operation.
|
||||||
|
/// </summary>
|
||||||
|
private const int low_priority_import_batch_size = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A singleton scheduler shared by all <see cref="RealmArchiveModelImporter{TModel}"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This scheduler generally performs IO and CPU intensive work so concurrency is limited harshly.
|
||||||
|
/// It is mainly being used as a queue mechanism for large imports.
|
||||||
|
/// </remarks>
|
||||||
|
private static readonly ThreadedTaskScheduler import_scheduler = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(RealmArchiveModelImporter<TModel>));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A second scheduler for lower priority imports.
|
||||||
|
/// For simplicity, these will just run in parallel with normal priority imports, but a future refactor would see this implemented via a custom scheduler/queue.
|
||||||
|
/// See https://gist.github.com/peppy/f0e118a14751fc832ca30dd48ba3876b for an incomplete version of this.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly ThreadedTaskScheduler import_scheduler_low_priority = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(RealmArchiveModelImporter<TModel>));
|
||||||
|
|
||||||
|
public virtual IEnumerable<string> HandledExtensions => new[] { @".zip" };
|
||||||
|
|
||||||
|
protected readonly RealmFileStore Files;
|
||||||
|
|
||||||
|
protected readonly RealmContextFactory ContextFactory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fired when the user requests to view the resulting import.
|
||||||
|
/// </summary>
|
||||||
|
public Action<IEnumerable<ILive<TModel>>>? PostImport { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set an endpoint for notifications to be posted to.
|
||||||
|
/// </summary>
|
||||||
|
public Action<Notification>? PostNotification { protected get; set; }
|
||||||
|
|
||||||
|
protected RealmArchiveModelImporter(Storage storage, RealmContextFactory contextFactory)
|
||||||
|
{
|
||||||
|
ContextFactory = contextFactory;
|
||||||
|
|
||||||
|
Files = new RealmFileStore(contextFactory, storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Import one or more <typeparamref name="TModel"/> items from filesystem <paramref name="paths"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This will be treated as a low priority import if more than one path is specified; use <see cref="Import(ImportTask[])"/> to always import at standard priority.
|
||||||
|
/// This will post notifications tracking progress.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="paths">One or more archive locations on disk.</param>
|
||||||
|
public Task Import(params string[] paths)
|
||||||
|
{
|
||||||
|
var notification = new ProgressNotification { State = ProgressNotificationState.Active };
|
||||||
|
|
||||||
|
PostNotification?.Invoke(notification);
|
||||||
|
|
||||||
|
return Import(notification, paths.Select(p => new ImportTask(p)).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Import(params ImportTask[] tasks)
|
||||||
|
{
|
||||||
|
var notification = new ProgressNotification { State = ProgressNotificationState.Active };
|
||||||
|
|
||||||
|
PostNotification?.Invoke(notification);
|
||||||
|
|
||||||
|
return Import(notification, tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<ILive<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks)
|
||||||
|
{
|
||||||
|
if (tasks.Length == 0)
|
||||||
|
{
|
||||||
|
notification.CompletionText = $"No {HumanisedModelName}s were found to import!";
|
||||||
|
notification.State = ProgressNotificationState.Completed;
|
||||||
|
return Enumerable.Empty<RealmLive<TModel>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.Progress = 0;
|
||||||
|
notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising...";
|
||||||
|
|
||||||
|
int current = 0;
|
||||||
|
|
||||||
|
var imported = new List<ILive<TModel>>();
|
||||||
|
|
||||||
|
bool isLowPriorityImport = tasks.Length > low_priority_import_batch_size;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.WhenAll(tasks.Select(async task =>
|
||||||
|
{
|
||||||
|
notification.CancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var model = await Import(task, isLowPriorityImport, notification.CancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
lock (imported)
|
||||||
|
{
|
||||||
|
if (model != null)
|
||||||
|
imported.Add(model);
|
||||||
|
current++;
|
||||||
|
|
||||||
|
notification.Text = $"Imported {current} of {tasks.Length} {HumanisedModelName}s";
|
||||||
|
notification.Progress = (float)current / tasks.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error(e, $@"Could not import ({task})", LoggingTarget.Database);
|
||||||
|
}
|
||||||
|
})).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
if (imported.Count == 0)
|
||||||
|
{
|
||||||
|
notification.State = ProgressNotificationState.Cancelled;
|
||||||
|
return imported;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imported.Count == 0)
|
||||||
|
{
|
||||||
|
notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import failed!";
|
||||||
|
notification.State = ProgressNotificationState.Cancelled;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
notification.CompletionText = imported.Count == 1
|
||||||
|
? $"Imported {imported.First()}!"
|
||||||
|
: $"Imported {imported.Count} {HumanisedModelName}s!";
|
||||||
|
|
||||||
|
if (imported.Count > 0 && PostImport != null)
|
||||||
|
{
|
||||||
|
notification.CompletionText += " Click to view.";
|
||||||
|
notification.CompletionClickAction = () =>
|
||||||
|
{
|
||||||
|
PostImport?.Invoke(imported);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.State = ProgressNotificationState.Completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return imported;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Import one <typeparamref name="TModel"/> from the filesystem and delete the file on success.
|
||||||
|
/// Note that this bypasses the UI flow and should only be used for special cases or testing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="task">The <see cref="ImportTask"/> containing data about the <typeparamref name="TModel"/> to import.</param>
|
||||||
|
/// <param name="lowPriority">Whether this is a low priority import.</param>
|
||||||
|
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||||
|
/// <returns>The imported model, if successful.</returns>
|
||||||
|
public async Task<ILive<TModel>?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
ILive<TModel>? import;
|
||||||
|
using (ArchiveReader reader = task.GetReader())
|
||||||
|
import = await Import(reader, lowPriority, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// We may or may not want to delete the file depending on where it is stored.
|
||||||
|
// e.g. reconstructing/repairing database with items from default storage.
|
||||||
|
// Also, not always a single file, i.e. for LegacyFilesystemReader
|
||||||
|
// TODO: Add a check to prevent files from storage to be deleted.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (import != null && File.Exists(task.Path) && ShouldDeleteArchive(task.Path))
|
||||||
|
File.Delete(task.Path);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error(e, $@"Could not delete original file after import ({task})");
|
||||||
|
}
|
||||||
|
|
||||||
|
return import;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Silently import an item from an <see cref="ArchiveReader"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="archive">The archive to be imported.</param>
|
||||||
|
/// <param name="lowPriority">Whether this is a low priority import.</param>
|
||||||
|
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||||
|
public async Task<ILive<TModel>?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
TModel? model = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
model = CreateModel(archive);
|
||||||
|
|
||||||
|
if (model == null)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LogForModel(model, @$"Model creation of {archive.Name} failed.", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scheduledImport = Task.Factory.StartNew(async () => await Import(model, archive, lowPriority, cancellationToken).ConfigureAwait(false),
|
||||||
|
cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap();
|
||||||
|
|
||||||
|
return await scheduledImport.ConfigureAwait(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Any file extensions which should be included in hash creation.
|
||||||
|
/// Generally should include all file types which determine the file's uniqueness.
|
||||||
|
/// Large files should be avoided if possible.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is only used by the default hash implementation. If <see cref="ComputeHash"/> is overridden, it will not be used.
|
||||||
|
/// </remarks>
|
||||||
|
protected abstract string[] HashableFileTypes { get; }
|
||||||
|
|
||||||
|
internal static void LogForModel(TModel? model, string message, Exception? e = null)
|
||||||
|
{
|
||||||
|
string trimmedHash;
|
||||||
|
if (model == null || !model.IsValid || string.IsNullOrEmpty(model.Hash))
|
||||||
|
trimmedHash = "?????";
|
||||||
|
else
|
||||||
|
trimmedHash = model.Hash.Substring(0, 5);
|
||||||
|
|
||||||
|
string prefix = $"[{trimmedHash}]";
|
||||||
|
|
||||||
|
if (e != null)
|
||||||
|
Logger.Error(e, $"{prefix} {message}", LoggingTarget.Database);
|
||||||
|
else
|
||||||
|
Logger.Log($"{prefix} {message}", LoggingTarget.Database);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the implementation overrides <see cref="ComputeHash"/> with a custom implementation.
|
||||||
|
/// Custom hash implementations must bypass the early exit in the import flow (see <see cref="computeHashFast"/> usage).
|
||||||
|
/// </summary>
|
||||||
|
protected virtual bool HasCustomHashFunction => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a SHA-2 hash from the provided archive based on file content of all files matching <see cref="HashableFileTypes"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// In the case of no matching files, a hash will be generated from the passed archive's <see cref="ArchiveReader.Name"/>.
|
||||||
|
/// </remarks>
|
||||||
|
protected virtual string ComputeHash(TModel item, ArchiveReader? reader = null)
|
||||||
|
{
|
||||||
|
if (reader != null)
|
||||||
|
// fast hashing for cases where the item's files may not be populated.
|
||||||
|
return computeHashFast(reader);
|
||||||
|
|
||||||
|
// for now, concatenate all hashable files in the set to create a unique hash.
|
||||||
|
MemoryStream hashable = new MemoryStream();
|
||||||
|
|
||||||
|
foreach (RealmNamedFileUsage file in item.Files.Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f.Filename))
|
||||||
|
{
|
||||||
|
using (Stream s = Files.Store.GetStream(file.File.StoragePath))
|
||||||
|
s.CopyTo(hashable);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashable.Length > 0)
|
||||||
|
return hashable.ComputeSHA2Hash();
|
||||||
|
|
||||||
|
return item.Hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Silently import an item from a <typeparamref name="TModel"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The model to be imported.</param>
|
||||||
|
/// <param name="archive">An optional archive to use for model population.</param>
|
||||||
|
/// <param name="lowPriority">Whether this is a low priority import.</param>
|
||||||
|
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||||
|
public virtual async Task<ILive<TModel>?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using (var realm = ContextFactory.CreateContext())
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
bool checkedExisting = false;
|
||||||
|
TModel? existing = null;
|
||||||
|
|
||||||
|
if (archive != null && !HasCustomHashFunction)
|
||||||
|
{
|
||||||
|
// this is a fast bail condition to improve large import performance.
|
||||||
|
item.Hash = computeHashFast(archive);
|
||||||
|
|
||||||
|
checkedExisting = true;
|
||||||
|
existing = CheckForExisting(item, realm);
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
// bare minimum comparisons
|
||||||
|
//
|
||||||
|
// note that this should really be checking filesizes on disk (of existing files) for some degree of sanity.
|
||||||
|
// or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files.
|
||||||
|
if (CanSkipImport(existing, item) &&
|
||||||
|
getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f)))
|
||||||
|
{
|
||||||
|
LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import.");
|
||||||
|
|
||||||
|
using (var transaction = realm.BeginWrite())
|
||||||
|
{
|
||||||
|
existing.DeletePending = false;
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing.ToLive();
|
||||||
|
}
|
||||||
|
|
||||||
|
LogForModel(item, @"Found existing (optimised) but failed pre-check.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LogForModel(item, @"Beginning import...");
|
||||||
|
|
||||||
|
// TODO: do we want to make the transaction this local? not 100% sure, will need further investigation.
|
||||||
|
using (var transaction = realm.BeginWrite())
|
||||||
|
{
|
||||||
|
if (archive != null)
|
||||||
|
// TODO: look into rollback of file additions (or delayed commit).
|
||||||
|
item.Files.AddRange(createFileInfos(archive, Files, realm));
|
||||||
|
|
||||||
|
item.Hash = ComputeHash(item, archive);
|
||||||
|
|
||||||
|
// TODO: we may want to run this outside of the transaction.
|
||||||
|
await Populate(item, archive, realm, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!checkedExisting)
|
||||||
|
existing = CheckForExisting(item, realm);
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
if (CanReuseExisting(existing, item))
|
||||||
|
{
|
||||||
|
LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import.");
|
||||||
|
existing.DeletePending = false;
|
||||||
|
|
||||||
|
return existing.ToLive();
|
||||||
|
}
|
||||||
|
|
||||||
|
LogForModel(item, @"Found existing but failed re-use check.");
|
||||||
|
|
||||||
|
existing.DeletePending = true;
|
||||||
|
|
||||||
|
// todo: actually delete? i don't think this is required...
|
||||||
|
// ModelStore.PurgeDeletable(s => s.ID == existing.ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
PreImport(item, realm);
|
||||||
|
|
||||||
|
// import to store
|
||||||
|
realm.Add(item);
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
LogForModel(item, @"Import successfully completed!");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (!(e is TaskCanceledException))
|
||||||
|
LogForModel(item, @"Database import or population failed and has been rolled back.", e);
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.ToLive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string computeHashFast(ArchiveReader reader)
|
||||||
|
{
|
||||||
|
MemoryStream hashable = new MemoryStream();
|
||||||
|
|
||||||
|
foreach (var file in reader.Filenames.Where(f => HashableFileTypes.Any(ext => f.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f))
|
||||||
|
{
|
||||||
|
using (Stream s = reader.GetStream(file))
|
||||||
|
s.CopyTo(hashable);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashable.Length > 0)
|
||||||
|
return hashable.ComputeSHA2Hash();
|
||||||
|
|
||||||
|
return reader.Name.ComputeSHA2Hash();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create all required <see cref="File"/>s for the provided archive, adding them to the global file store.
|
||||||
|
/// </summary>
|
||||||
|
private List<RealmNamedFileUsage> createFileInfos(ArchiveReader reader, RealmFileStore files, Realm realm)
|
||||||
|
{
|
||||||
|
var fileInfos = new List<RealmNamedFileUsage>();
|
||||||
|
|
||||||
|
// import files to manager
|
||||||
|
foreach (var filenames in getShortenedFilenames(reader))
|
||||||
|
{
|
||||||
|
using (Stream s = reader.GetStream(filenames.original))
|
||||||
|
{
|
||||||
|
var item = new RealmNamedFileUsage(files.Add(s, realm), filenames.shortened);
|
||||||
|
fileInfos.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileInfos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<(string original, string shortened)> getShortenedFilenames(ArchiveReader reader)
|
||||||
|
{
|
||||||
|
string prefix = reader.Filenames.GetCommonPrefix();
|
||||||
|
if (!(prefix.EndsWith('/') || prefix.EndsWith('\\')))
|
||||||
|
prefix = string.Empty;
|
||||||
|
|
||||||
|
// import files to manager
|
||||||
|
foreach (string file in reader.Filenames)
|
||||||
|
yield return (file, file.Substring(prefix.Length).ToStandardisedPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a barebones model from the provided archive.
|
||||||
|
/// Actual expensive population should be done in <see cref="Populate"/>; this should just prepare for duplicate checking.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="archive">The archive to create the model for.</param>
|
||||||
|
/// <returns>A model populated with minimal information. Returning a null will abort importing silently.</returns>
|
||||||
|
protected abstract TModel? CreateModel(ArchiveReader archive);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populate the provided model completely from the given archive.
|
||||||
|
/// After this method, the model should be in a state ready to commit to a store.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">The model to populate.</param>
|
||||||
|
/// <param name="archive">The archive to use as a reference for population. May be null.</param>
|
||||||
|
/// <param name="realm">The current realm context.</param>
|
||||||
|
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||||
|
protected abstract Task Populate(TModel model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Perform any final actions before the import to database executes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">The model prepared for import.</param>
|
||||||
|
/// <param name="realm">The current realm context.</param>
|
||||||
|
protected virtual void PreImport(TModel model, Realm realm)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether an existing model already exists for a new import item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">The new model proposed for import.</param>
|
||||||
|
/// <param name="realm">The current realm context.</param>
|
||||||
|
/// <returns>An existing model which matches the criteria to skip importing, else null.</returns>
|
||||||
|
protected TModel? CheckForExisting(TModel model, Realm realm) => string.IsNullOrEmpty(model.Hash) ? null : realm.All<TModel>().FirstOrDefault(b => b.Hash == model.Hash);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether import can be skipped after finding an existing import early in the process.
|
||||||
|
/// Only valid when <see cref="ComputeHash"/> is not overridden.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="existing">The existing model.</param>
|
||||||
|
/// <param name="import">The newly imported model.</param>
|
||||||
|
/// <returns>Whether to skip this import completely.</returns>
|
||||||
|
protected virtual bool CanSkipImport(TModel existing, TModel import) => true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// After an existing <typeparamref name="TModel"/> is found during an import process, the default behaviour is to use/restore the existing
|
||||||
|
/// item and skip the import. This method allows changing that behaviour.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="existing">The existing model.</param>
|
||||||
|
/// <param name="import">The newly imported model.</param>
|
||||||
|
/// <returns>Whether the existing model should be restored and used. Returning false will delete the existing and force a re-import.</returns>
|
||||||
|
protected virtual bool CanReuseExisting(TModel existing, TModel import) =>
|
||||||
|
// for the best or worst, we copy and import files of a new import before checking whether
|
||||||
|
// it is a duplicate. so to check if anything has changed, we can just compare all File IDs.
|
||||||
|
getIDs(existing.Files).SequenceEqual(getIDs(import.Files)) &&
|
||||||
|
getFilenames(existing.Files).SequenceEqual(getFilenames(import.Files));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this specified path should be removed after successful import.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path for consideration. May be a file or a directory.</param>
|
||||||
|
/// <returns>Whether to perform deletion.</returns>
|
||||||
|
protected virtual bool ShouldDeleteArchive(string path) => false;
|
||||||
|
|
||||||
|
private IEnumerable<string> getIDs(IEnumerable<INamedFile> files)
|
||||||
|
{
|
||||||
|
foreach (var f in files.OrderBy(f => f.Filename))
|
||||||
|
yield return f.File.Hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<string> getFilenames(IEnumerable<INamedFile> files)
|
||||||
|
{
|
||||||
|
foreach (var f in files.OrderBy(f => f.Filename))
|
||||||
|
yield return f.Filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}";
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ using System.Text;
|
|||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
|
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
|
||||||
|
|
||||||
@ -32,7 +33,7 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
BeatmapInfo.BeatmapSet.Beatmaps = new List<BeatmapInfo> { BeatmapInfo };
|
BeatmapInfo.BeatmapSet.Beatmaps = new List<BeatmapInfo> { BeatmapInfo };
|
||||||
BeatmapInfo.Length = 75000;
|
BeatmapInfo.Length = 75000;
|
||||||
BeatmapInfo.OnlineInfo = new BeatmapOnlineInfo();
|
BeatmapInfo.OnlineInfo = new BeatmapOnlineInfo();
|
||||||
BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo
|
BeatmapInfo.BeatmapSet.OnlineInfo = new APIBeatmapSet
|
||||||
{
|
{
|
||||||
Status = BeatmapSetOnlineStatus.Ranked,
|
Status = BeatmapSetOnlineStatus.Ranked,
|
||||||
Covers = new BeatmapSetOnlineCovers
|
Covers = new BeatmapSetOnlineCovers
|
||||||
|
Loading…
x
Reference in New Issue
Block a user