Merge branch 'master' into fix-legacy-score-display-fixed-width

This commit is contained in:
Dan Balasescu 2020-10-19 19:04:20 +09:00 committed by GitHub
commit 534088c66b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 852 additions and 265 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1016.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1009.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1019.0" />
</ItemGroup>
</Project>

View File

@ -145,11 +145,19 @@ namespace osu.Game.Rulesets.Catch.UI
}
};
trailsTarget.Add(trails = new CatcherTrailDisplay(this));
trails = new CatcherTrailDisplay(this);
updateCatcher();
}
protected override void LoadComplete()
{
base.LoadComplete();
// don't add in above load as we may potentially modify a parent in an unsafe manner.
trailsTarget.Add(trails);
}
/// <summary>
/// Creates proxied content to be displayed beneath hitobjects.
/// </summary>

View File

@ -111,6 +111,7 @@ namespace osu.Game.Tests.NonVisual
var osu = LoadOsuIntoHost(host);
var storage = osu.Dependencies.Get<Storage>();
var osuStorage = storage as MigratableStorage;
// Store the current storage's path. We'll need to refer to this for assertions in the original directory after the migration completes.
string originalDirectory = storage.GetFullPath(".");
@ -137,13 +138,15 @@ namespace osu.Game.Tests.NonVisual
Assert.That(!Directory.Exists(Path.Combine(originalDirectory, "test-nested", "cache")));
Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
foreach (var file in OsuStorage.IGNORE_FILES)
Assert.That(osuStorage, Is.Not.Null);
foreach (var file in osuStorage.IgnoreFiles)
{
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
Assert.That(storage.Exists(file), Is.False);
}
foreach (var dir in OsuStorage.IGNORE_DIRECTORIES)
foreach (var dir in osuStorage.IgnoreDirectories)
{
Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir)));
Assert.That(storage.ExistsDirectory(dir), Is.False);

View File

@ -53,5 +53,263 @@ namespace osu.Game.Tests.Rulesets.Scoring
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value));
}
/// <summary>
/// Test to see that all <see cref="HitResult"/>s contribute to score portions in correct amounts.
/// </summary>
/// <param name="scoringMode">Scoring mode to test.</param>
/// <param name="hitResult">The <see cref="HitResult"/> that will be applied to selected hit objects.</param>
/// <param name="maxResult">The maximum <see cref="HitResult"/> achievable.</param>
/// <param name="expectedScore">Expected score after all objects have been judged, rounded to the nearest integer.</param>
/// <remarks>
/// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo.
/// <para>
/// For standardised scoring, <paramref name="expectedScore"/> is calculated using the following formula:
/// 1_000_000 * (((3 * <paramref name="hitResult"/>) / (4 * <paramref name="maxResult"/>)) * 30% + (bestCombo / maxCombo) * 70%)
/// </para>
/// <para>
/// For classic scoring, <paramref name="expectedScore"/> is calculated using the following formula:
/// <paramref name="hitResult"/> / <paramref name="maxResult"/> * 936
/// where 936 is simplified from:
/// 75% * 4 * 300 * (1 + 1/25)
/// </para>
/// </remarks>
[TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] // (3 * 0) / (4 * 300) * 300_000 + (0 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] // (3 * 50) / (4 * 300) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] // (3 * 100) / (4 * 300) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 478_571)] // (3 * 200) / (4 * 350) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] // (3 * 300) / (4 * 300) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] // (3 * 350) / (4 * 350) * 300_000 + (2 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 700_000)] // (3 * 0) / (4 * 10) * 300_000 + 700_000 (max combo 0)
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 925_000)] // (3 * 10) / (4 * 10) * 300_000 + 700_000 (max combo 0)
[TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (3 * 0) / (4 * 30) * 300_000 + (0 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] // (3 * 30) / (4 * 30) * 300_000 + (0 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 700_030)] // 0 * 300_000 + 700_000 (max combo 0) + 3 * 10 (bonus points)
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 700_150)] // 0 * 300_000 + 700_000 (max combo 0) + 3 * 50 (bonus points)
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 156)] // (((3 * 50) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 312)] // (((3 * 100) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 535)] // (((3 * 200) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 936)] // (((3 * 300) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 936)] // (((3 * 350) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] // (0 * 1 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 225)] // (((3 * 10) / (4 * 10)) * 1 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 936)] // (((3 * 50) / (4 * 50)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 30)] // (0 * 1 * 300) * (1 + 0 / 25) + 3 * 10 (bonus points)
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 150)] // (0 * 1 * 300) * (1 + 0 / 25) * 3 * 50 (bonus points)
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
{
var minResult = new TestJudgement(hitResult).MinResult;
IBeatmap fourObjectBeatmap = new TestBeatmap(new RulesetInfo())
{
HitObjects = new List<HitObject>(Enumerable.Repeat(new TestHitObject(maxResult), 4))
};
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(fourObjectBeatmap);
for (int i = 0; i < 4; i++)
{
var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new Judgement())
{
Type = i == 2 ? minResult : hitResult
};
scoreProcessor.ApplyResult(judgementResult);
}
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value, 0.5));
}
/// <remarks>
/// This test uses a beatmap with four small ticks and one object with the <see cref="Judgement.MaxResult"/> of <see cref="HitResult.Ok"/>.
/// Its goal is to ensure that with the <see cref="ScoringMode"/> of <see cref="ScoringMode.Standardised"/>,
/// small ticks contribute to the accuracy portion, but not the combo portion.
/// In contrast, <see cref="ScoringMode.Classic"/> does not have separate combo and accuracy portion (they are multiplied by each other).
/// </remarks>
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 978_571)] // (3 * 10 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 914_286)] // (3 * 0 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 279)] // (((3 * 10 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 214)] // (((3 * 0 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
public void TestSmallTicksAccuracy(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
{
IEnumerable<HitObject> hitObjects = Enumerable
.Repeat(new TestHitObject(HitResult.SmallTickHit), 4)
.Append(new TestHitObject(HitResult.Ok));
IBeatmap fiveObjectBeatmap = new TestBeatmap(new RulesetInfo())
{
HitObjects = hitObjects.ToList()
};
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(fiveObjectBeatmap);
for (int i = 0; i < 4; i++)
{
var judgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects[i], new Judgement())
{
Type = i == 2 ? HitResult.SmallTickMiss : hitResult
};
scoreProcessor.ApplyResult(judgementResult);
}
var lastJudgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects.Last(), new Judgement())
{
Type = HitResult.Ok
};
scoreProcessor.ApplyResult(lastJudgementResult);
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value, 0.5));
}
[Test]
public void TestEmptyBeatmap(
[Values(ScoringMode.Standardised, ScoringMode.Classic)]
ScoringMode scoringMode)
{
scoreProcessor.Mode.Value = scoringMode;
scoreProcessor.ApplyBeatmap(new TestBeatmap(new RulesetInfo()));
Assert.IsTrue(Precision.AlmostEquals(0, scoreProcessor.TotalScore.Value));
}
[TestCase(HitResult.IgnoreHit, HitResult.IgnoreMiss)]
[TestCase(HitResult.Meh, HitResult.Miss)]
[TestCase(HitResult.Ok, HitResult.Miss)]
[TestCase(HitResult.Good, HitResult.Miss)]
[TestCase(HitResult.Great, HitResult.Miss)]
[TestCase(HitResult.Perfect, HitResult.Miss)]
[TestCase(HitResult.SmallTickHit, HitResult.SmallTickMiss)]
[TestCase(HitResult.LargeTickHit, HitResult.LargeTickMiss)]
[TestCase(HitResult.SmallBonus, HitResult.IgnoreMiss)]
[TestCase(HitResult.LargeBonus, HitResult.IgnoreMiss)]
public void TestMinResults(HitResult hitResult, HitResult expectedMinResult)
{
Assert.AreEqual(expectedMinResult, new TestJudgement(hitResult).MinResult);
}
[TestCase(HitResult.None, false)]
[TestCase(HitResult.IgnoreMiss, false)]
[TestCase(HitResult.IgnoreHit, false)]
[TestCase(HitResult.Miss, true)]
[TestCase(HitResult.Meh, true)]
[TestCase(HitResult.Ok, true)]
[TestCase(HitResult.Good, true)]
[TestCase(HitResult.Great, true)]
[TestCase(HitResult.Perfect, true)]
[TestCase(HitResult.SmallTickMiss, false)]
[TestCase(HitResult.SmallTickHit, false)]
[TestCase(HitResult.LargeTickMiss, true)]
[TestCase(HitResult.LargeTickHit, true)]
[TestCase(HitResult.SmallBonus, false)]
[TestCase(HitResult.LargeBonus, false)]
public void TestAffectsCombo(HitResult hitResult, bool expectedReturnValue)
{
Assert.AreEqual(expectedReturnValue, hitResult.AffectsCombo());
}
[TestCase(HitResult.None, false)]
[TestCase(HitResult.IgnoreMiss, false)]
[TestCase(HitResult.IgnoreHit, false)]
[TestCase(HitResult.Miss, true)]
[TestCase(HitResult.Meh, true)]
[TestCase(HitResult.Ok, true)]
[TestCase(HitResult.Good, true)]
[TestCase(HitResult.Great, true)]
[TestCase(HitResult.Perfect, true)]
[TestCase(HitResult.SmallTickMiss, true)]
[TestCase(HitResult.SmallTickHit, true)]
[TestCase(HitResult.LargeTickMiss, true)]
[TestCase(HitResult.LargeTickHit, true)]
[TestCase(HitResult.SmallBonus, false)]
[TestCase(HitResult.LargeBonus, false)]
public void TestAffectsAccuracy(HitResult hitResult, bool expectedReturnValue)
{
Assert.AreEqual(expectedReturnValue, hitResult.AffectsAccuracy());
}
[TestCase(HitResult.None, false)]
[TestCase(HitResult.IgnoreMiss, false)]
[TestCase(HitResult.IgnoreHit, false)]
[TestCase(HitResult.Miss, false)]
[TestCase(HitResult.Meh, false)]
[TestCase(HitResult.Ok, false)]
[TestCase(HitResult.Good, false)]
[TestCase(HitResult.Great, false)]
[TestCase(HitResult.Perfect, false)]
[TestCase(HitResult.SmallTickMiss, false)]
[TestCase(HitResult.SmallTickHit, false)]
[TestCase(HitResult.LargeTickMiss, false)]
[TestCase(HitResult.LargeTickHit, false)]
[TestCase(HitResult.SmallBonus, true)]
[TestCase(HitResult.LargeBonus, true)]
public void TestIsBonus(HitResult hitResult, bool expectedReturnValue)
{
Assert.AreEqual(expectedReturnValue, hitResult.IsBonus());
}
[TestCase(HitResult.None, false)]
[TestCase(HitResult.IgnoreMiss, false)]
[TestCase(HitResult.IgnoreHit, true)]
[TestCase(HitResult.Miss, false)]
[TestCase(HitResult.Meh, true)]
[TestCase(HitResult.Ok, true)]
[TestCase(HitResult.Good, true)]
[TestCase(HitResult.Great, true)]
[TestCase(HitResult.Perfect, true)]
[TestCase(HitResult.SmallTickMiss, false)]
[TestCase(HitResult.SmallTickHit, true)]
[TestCase(HitResult.LargeTickMiss, false)]
[TestCase(HitResult.LargeTickHit, true)]
[TestCase(HitResult.SmallBonus, true)]
[TestCase(HitResult.LargeBonus, true)]
public void TestIsHit(HitResult hitResult, bool expectedReturnValue)
{
Assert.AreEqual(expectedReturnValue, hitResult.IsHit());
}
[TestCase(HitResult.None, false)]
[TestCase(HitResult.IgnoreMiss, false)]
[TestCase(HitResult.IgnoreHit, false)]
[TestCase(HitResult.Miss, true)]
[TestCase(HitResult.Meh, true)]
[TestCase(HitResult.Ok, true)]
[TestCase(HitResult.Good, true)]
[TestCase(HitResult.Great, true)]
[TestCase(HitResult.Perfect, true)]
[TestCase(HitResult.SmallTickMiss, true)]
[TestCase(HitResult.SmallTickHit, true)]
[TestCase(HitResult.LargeTickMiss, true)]
[TestCase(HitResult.LargeTickHit, true)]
[TestCase(HitResult.SmallBonus, true)]
[TestCase(HitResult.LargeBonus, true)]
public void TestIsScorable(HitResult hitResult, bool expectedReturnValue)
{
Assert.AreEqual(expectedReturnValue, hitResult.IsScorable());
}
private class TestJudgement : Judgement
{
public override HitResult MaxResult { get; }
public TestJudgement(HitResult maxResult)
{
MaxResult = maxResult;
}
}
private class TestHitObject : HitObject
{
private readonly HitResult maxResult;
public override Judgement CreateJudgement()
{
return new TestJudgement(maxResult);
}
public TestHitObject(HitResult maxResult)
{
this.maxResult = maxResult;
}
}
}
}

View File

@ -0,0 +1,169 @@
// 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.IO;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Platform;
using osu.Game.Tournament.Configuration;
using osu.Game.Tests;
namespace osu.Game.Tournament.Tests.NonVisual
{
[TestFixture]
public class CustomTourneyDirectoryTest
{
[Test]
public void TestDefaultDirectory()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = loadOsu(host);
var storage = osu.Dependencies.Get<Storage>();
Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default")));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestCustomDirectory()
{
using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestCustomDirectory))) // don't use clean run as we are writing a config file.
{
string osuDesktopStorage = basePath(nameof(TestCustomDirectory));
const string custom_tournament = "custom";
// need access before the game has constructed its own storage yet.
Storage storage = new DesktopStorage(osuDesktopStorage, host);
// manual cleaning so we can prepare a config file.
storage.DeleteDirectory(string.Empty);
using (var storageConfig = new TournamentStorageManager(storage))
storageConfig.Set(StorageConfig.CurrentTournament, custom_tournament);
try
{
var osu = loadOsu(host);
storage = osu.Dependencies.Get<Storage>();
Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(host.Storage.GetFullPath("."), "tournaments", custom_tournament)));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestMigration()
{
using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestMigration))) // don't use clean run as we are writing test files for migration.
{
string osuRoot = basePath(nameof(TestMigration));
string configFile = Path.Combine(osuRoot, "tournament.ini");
if (File.Exists(configFile))
File.Delete(configFile);
// Recreate the old setup that uses "tournament" as the base path.
string oldPath = Path.Combine(osuRoot, "tournament");
string videosPath = Path.Combine(oldPath, "videos");
string modsPath = Path.Combine(oldPath, "mods");
string flagsPath = Path.Combine(oldPath, "flags");
Directory.CreateDirectory(videosPath);
Directory.CreateDirectory(modsPath);
Directory.CreateDirectory(flagsPath);
// Define testing files corresponding to the specific file migrations that are needed
string bracketFile = Path.Combine(osuRoot, "bracket.json");
string drawingsConfig = Path.Combine(osuRoot, "drawings.ini");
string drawingsFile = Path.Combine(osuRoot, "drawings.txt");
string drawingsResult = Path.Combine(osuRoot, "drawings_results.txt");
// Define sample files to test recursive copying
string videoFile = Path.Combine(videosPath, "video.mp4");
string modFile = Path.Combine(modsPath, "mod.png");
string flagFile = Path.Combine(flagsPath, "flag.png");
File.WriteAllText(bracketFile, "{}");
File.WriteAllText(drawingsConfig, "test");
File.WriteAllText(drawingsFile, "test");
File.WriteAllText(drawingsResult, "test");
File.WriteAllText(videoFile, "test");
File.WriteAllText(modFile, "test");
File.WriteAllText(flagFile, "test");
try
{
var osu = loadOsu(host);
var storage = osu.Dependencies.Get<Storage>();
string migratedPath = Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default");
videosPath = Path.Combine(migratedPath, "videos");
modsPath = Path.Combine(migratedPath, "mods");
flagsPath = Path.Combine(migratedPath, "flags");
videoFile = Path.Combine(videosPath, "video.mp4");
modFile = Path.Combine(modsPath, "mod.png");
flagFile = Path.Combine(flagsPath, "flag.png");
Assert.That(storage.GetFullPath("."), Is.EqualTo(migratedPath));
Assert.True(storage.Exists("bracket.json"));
Assert.True(storage.Exists("drawings.txt"));
Assert.True(storage.Exists("drawings_results.txt"));
Assert.True(storage.Exists("drawings.ini"));
Assert.True(storage.Exists(videoFile));
Assert.True(storage.Exists(modFile));
Assert.True(storage.Exists(flagFile));
}
finally
{
host.Storage.Delete("tournament.ini");
host.Storage.DeleteDirectory("tournaments");
host.Exit();
}
}
}
private TournamentGameBase loadOsu(GameHost host)
{
var osu = new TournamentGameBase();
Task.Run(() => host.Run(osu));
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return osu;
}
private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 90000)
{
Task task = Task.Run(() =>
{
while (!result()) Thread.Sleep(200);
});
Assert.IsTrue(task.Wait(timeout), failureMessage);
}
private string basePath(string testInstance) => Path.Combine(RuntimeInfo.StartupDirectory, "headless", testInstance);
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Video;
using osu.Framework.Timing;
using osu.Game.Graphics;
using osu.Game.Tournament.IO;
namespace osu.Game.Tournament.Components
{
@ -17,7 +18,6 @@ namespace osu.Game.Tournament.Components
private readonly string filename;
private readonly bool drawFallbackGradient;
private Video video;
private ManualClock manualClock;
public TourneyVideo(string filename, bool drawFallbackGradient = false)
@ -27,9 +27,9 @@ namespace osu.Game.Tournament.Components
}
[BackgroundDependencyLoader]
private void load(TournamentStorage storage)
private void load(TournamentVideoResourceStore storage)
{
var stream = storage.GetStream($@"videos/{filename}");
var stream = storage.GetStream(filename);
if (stream != null)
{

View File

@ -0,0 +1,23 @@
// 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.Configuration;
using osu.Framework.Platform;
namespace osu.Game.Tournament.Configuration
{
public class TournamentStorageManager : IniConfigManager<StorageConfig>
{
protected override string Filename => "tournament.ini";
public TournamentStorageManager(Storage storage)
: base(storage)
{
}
}
public enum StorageConfig
{
CurrentTournament,
}
}

View File

@ -0,0 +1,72 @@
// 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.Logging;
using osu.Framework.Platform;
using osu.Game.IO;
using System.IO;
using osu.Game.Tournament.Configuration;
namespace osu.Game.Tournament.IO
{
public class TournamentStorage : MigratableStorage
{
private const string default_tournament = "default";
private readonly Storage storage;
private readonly TournamentStorageManager storageConfig;
public TournamentStorage(Storage storage)
: base(storage.GetStorageForDirectory("tournaments"), string.Empty)
{
this.storage = storage;
storageConfig = new TournamentStorageManager(storage);
if (storage.Exists("tournament.ini"))
{
ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(storageConfig.Get<string>(StorageConfig.CurrentTournament)));
}
else
Migrate(UnderlyingStorage.GetStorageForDirectory(default_tournament));
Logger.Log("Using tournament storage: " + GetFullPath(string.Empty));
}
public override void Migrate(Storage newStorage)
{
// this migration only happens once on moving to the per-tournament storage system.
// listed files are those known at that point in time.
// this can be removed at some point in the future (6 months obsoletion would mean 2021-04-19)
var source = new DirectoryInfo(storage.GetFullPath("tournament"));
var destination = new DirectoryInfo(newStorage.GetFullPath("."));
if (source.Exists)
{
Logger.Log("Migrating tournament assets to default tournament storage.");
CopyRecursive(source, destination);
DeleteRecursive(source);
}
moveFileIfExists("bracket.json", destination);
moveFileIfExists("drawings.txt", destination);
moveFileIfExists("drawings_results.txt", destination);
moveFileIfExists("drawings.ini", destination);
ChangeTargetStorage(newStorage);
storageConfig.Set(StorageConfig.CurrentTournament, default_tournament);
storageConfig.Save();
}
private void moveFileIfExists(string file, DirectoryInfo destination)
{
if (!storage.Exists(file))
return;
Logger.Log($"Migrating {file} to default tournament storage.");
var fileInfo = new System.IO.FileInfo(storage.GetFullPath(file));
AttemptOperation(() => fileInfo.CopyTo(Path.Combine(destination.FullName, fileInfo.Name), true));
fileInfo.Delete();
}
}
}

View File

@ -4,12 +4,12 @@
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
namespace osu.Game.Tournament
namespace osu.Game.Tournament.IO
{
internal class TournamentStorage : NamespacedResourceStore<byte[]>
public class TournamentVideoResourceStore : NamespacedResourceStore<byte[]>
{
public TournamentStorage(Storage storage)
: base(new StorageBackedResourceStore(storage), "tournament")
public TournamentVideoResourceStore(Storage storage)
: base(new StorageBackedResourceStore(storage), "videos")
{
AddExtension("m4v");
AddExtension("avi");

View File

@ -8,11 +8,12 @@ using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Framework.IO.Stores;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests;
using osu.Game.Tournament.IPC;
using osu.Game.Tournament.IO;
using osu.Game.Tournament.Models;
using osu.Game.Users;
using osuTK.Input;
@ -23,13 +24,8 @@ namespace osu.Game.Tournament
public class TournamentGameBase : OsuGameBase
{
private const string bracket_filename = "bracket.json";
private LadderInfo ladder;
private Storage storage;
private TournamentStorage tournamentStorage;
private TournamentStorage storage;
private DependencyContainer dependencies;
private FileBasedIPC ipc;
@ -39,15 +35,14 @@ namespace osu.Game.Tournament
}
[BackgroundDependencyLoader]
private void load(Storage storage)
private void load(Storage baseStorage)
{
Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly));
dependencies.CacheAs(tournamentStorage = new TournamentStorage(storage));
dependencies.CacheAs<Storage>(storage = new TournamentStorage(baseStorage));
dependencies.Cache(new TournamentVideoResourceStore(storage));
Textures.AddStore(new TextureLoaderStore(tournamentStorage));
this.storage = storage;
Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage)));
readBracket();

View File

@ -56,8 +56,7 @@ namespace osu.Game.Graphics.UserInterface
return;
displayedCount = value;
if (displayedCountSpriteText != null)
displayedCountSpriteText.Text = FormatCount(value);
UpdateDisplay();
}
}
@ -73,10 +72,17 @@ namespace osu.Game.Graphics.UserInterface
private void load()
{
displayedCountSpriteText = CreateSpriteText();
displayedCountSpriteText.Text = FormatCount(DisplayedCount);
UpdateDisplay();
Child = displayedCountSpriteText;
}
protected void UpdateDisplay()
{
if (displayedCountSpriteText != null)
displayedCountSpriteText.Text = FormatCount(DisplayedCount);
}
protected override void LoadComplete()
{
base.LoadComplete();

View File

@ -1,6 +1,7 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Play.HUD;
@ -17,20 +18,19 @@ namespace osu.Game.Graphics.UserInterface
/// </summary>
public bool UseCommaSeparator { get; }
/// <summary>
/// How many leading zeroes the counter has.
/// </summary>
public uint LeadingZeroes { get; }
public Bindable<int> RequiredDisplayDigits { get; } = new Bindable<int>();
/// <summary>
/// Displays score.
/// </summary>
/// <param name="leading">How many leading zeroes the counter will have.</param>
/// <param name="useCommaSeparator">Whether comma separators should be displayed.</param>
protected ScoreCounter(uint leading = 0, bool useCommaSeparator = false)
protected ScoreCounter(int leading = 0, bool useCommaSeparator = false)
{
UseCommaSeparator = useCommaSeparator;
LeadingZeroes = leading;
RequiredDisplayDigits.Value = leading;
RequiredDisplayDigits.BindValueChanged(_ => UpdateDisplay());
}
protected override double GetProportionalDuration(double currentValue, double newValue)
@ -40,7 +40,7 @@ namespace osu.Game.Graphics.UserInterface
protected override string FormatCount(double count)
{
string format = new string('0', (int)LeadingZeroes);
string format = new string('0', RequiredDisplayDigits.Value);
if (UseCommaSeparator)
{

View File

@ -73,8 +73,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
},
new Container
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
// top right works better when the vertical height of the component changes smoothly (avoids weird layout animations).
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = Component = CreateComponent().With(d =>

View File

@ -0,0 +1,132 @@
// 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.IO;
using System.Linq;
using System.Threading;
using osu.Framework.Platform;
namespace osu.Game.IO
{
/// <summary>
/// A <see cref="WrappedStorage"/> that is migratable to different locations.
/// </summary>
public abstract class MigratableStorage : WrappedStorage
{
/// <summary>
/// A relative list of directory paths which should not be migrated.
/// </summary>
public virtual string[] IgnoreDirectories => Array.Empty<string>();
/// <summary>
/// A relative list of file paths which should not be migrated.
/// </summary>
public virtual string[] IgnoreFiles => Array.Empty<string>();
protected MigratableStorage(Storage storage, string subPath = null)
: base(storage, subPath)
{
}
/// <summary>
/// A general purpose migration method to move the storage to a different location.
/// <param name="newStorage">The target storage of the migration.</param>
/// </summary>
public virtual void Migrate(Storage newStorage)
{
var source = new DirectoryInfo(GetFullPath("."));
var destination = new DirectoryInfo(newStorage.GetFullPath("."));
// using Uri is the easiest way to check equality and contains (https://stackoverflow.com/a/7710620)
var sourceUri = new Uri(source.FullName + Path.DirectorySeparatorChar);
var destinationUri = new Uri(destination.FullName + Path.DirectorySeparatorChar);
if (sourceUri == destinationUri)
throw new ArgumentException("Destination provided is already the current location", destination.FullName);
if (sourceUri.IsBaseOf(destinationUri))
throw new ArgumentException("Destination provided is inside the source", destination.FullName);
// ensure the new location has no files present, else hard abort
if (destination.Exists)
{
if (destination.GetFiles().Length > 0 || destination.GetDirectories().Length > 0)
throw new ArgumentException("Destination provided already has files or directories present", destination.FullName);
}
CopyRecursive(source, destination);
ChangeTargetStorage(newStorage);
DeleteRecursive(source);
}
protected void DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true)
{
foreach (System.IO.FileInfo fi in target.GetFiles())
{
if (topLevelExcludes && IgnoreFiles.Contains(fi.Name))
continue;
AttemptOperation(() => fi.Delete());
}
foreach (DirectoryInfo dir in target.GetDirectories())
{
if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name))
continue;
AttemptOperation(() => dir.Delete(true));
}
if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0)
AttemptOperation(target.Delete);
}
protected void CopyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true)
{
// based off example code https://docs.microsoft.com/en-us/dotnet/api/system.io.directoryinfo
if (!destination.Exists)
Directory.CreateDirectory(destination.FullName);
foreach (System.IO.FileInfo fi in source.GetFiles())
{
if (topLevelExcludes && IgnoreFiles.Contains(fi.Name))
continue;
AttemptOperation(() => fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true));
}
foreach (DirectoryInfo dir in source.GetDirectories())
{
if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name))
continue;
CopyRecursive(dir, destination.CreateSubdirectory(dir.Name), false);
}
}
/// <summary>
/// Attempt an IO operation multiple times and only throw if none of the attempts succeed.
/// </summary>
/// <param name="action">The action to perform.</param>
/// <param name="attempts">The number of attempts (250ms wait between each).</param>
protected static void AttemptOperation(Action action, int attempts = 10)
{
while (true)
{
try
{
action();
return;
}
catch (Exception)
{
if (attempts-- == 0)
throw;
}
Thread.Sleep(250);
}
}
}
}

View File

@ -1,11 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using JetBrains.Annotations;
using osu.Framework.Logging;
using osu.Framework.Platform;
@ -13,7 +10,7 @@ using osu.Game.Configuration;
namespace osu.Game.IO
{
public class OsuStorage : WrappedStorage
public class OsuStorage : MigratableStorage
{
/// <summary>
/// Indicates the error (if any) that occurred when initialising the custom storage during initial startup.
@ -36,9 +33,9 @@ namespace osu.Game.IO
private readonly StorageConfigManager storageConfig;
private readonly Storage defaultStorage;
public static readonly string[] IGNORE_DIRECTORIES = { "cache" };
public override string[] IgnoreDirectories => new[] { "cache" };
public static readonly string[] IGNORE_FILES =
public override string[] IgnoreFiles => new[]
{
"framework.ini",
"storage.ini"
@ -103,106 +100,11 @@ namespace osu.Game.IO
Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs");
}
public void Migrate(string newLocation)
public override void Migrate(Storage newStorage)
{
var source = new DirectoryInfo(GetFullPath("."));
var destination = new DirectoryInfo(newLocation);
// using Uri is the easiest way to check equality and contains (https://stackoverflow.com/a/7710620)
var sourceUri = new Uri(source.FullName + Path.DirectorySeparatorChar);
var destinationUri = new Uri(destination.FullName + Path.DirectorySeparatorChar);
if (sourceUri == destinationUri)
throw new ArgumentException("Destination provided is already the current location", nameof(newLocation));
if (sourceUri.IsBaseOf(destinationUri))
throw new ArgumentException("Destination provided is inside the source", nameof(newLocation));
// ensure the new location has no files present, else hard abort
if (destination.Exists)
{
if (destination.GetFiles().Length > 0 || destination.GetDirectories().Length > 0)
throw new ArgumentException("Destination provided already has files or directories present", nameof(newLocation));
deleteRecursive(destination);
}
copyRecursive(source, destination);
ChangeTargetStorage(host.GetStorage(newLocation));
storageConfig.Set(StorageConfig.FullPath, newLocation);
base.Migrate(newStorage);
storageConfig.Set(StorageConfig.FullPath, newStorage.GetFullPath("."));
storageConfig.Save();
deleteRecursive(source);
}
private static void deleteRecursive(DirectoryInfo target, bool topLevelExcludes = true)
{
foreach (System.IO.FileInfo fi in target.GetFiles())
{
if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name))
continue;
attemptOperation(() => fi.Delete());
}
foreach (DirectoryInfo dir in target.GetDirectories())
{
if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name))
continue;
attemptOperation(() => dir.Delete(true));
}
if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0)
attemptOperation(target.Delete);
}
private static void copyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true)
{
// based off example code https://docs.microsoft.com/en-us/dotnet/api/system.io.directoryinfo
Directory.CreateDirectory(destination.FullName);
foreach (System.IO.FileInfo fi in source.GetFiles())
{
if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name))
continue;
attemptOperation(() => fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true));
}
foreach (DirectoryInfo dir in source.GetDirectories())
{
if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name))
continue;
copyRecursive(dir, destination.CreateSubdirectory(dir.Name), false);
}
}
/// <summary>
/// Attempt an IO operation multiple times and only throw if none of the attempts succeed.
/// </summary>
/// <param name="action">The action to perform.</param>
/// <param name="attempts">The number of attempts (250ms wait between each).</param>
private static void attemptOperation(Action action, int attempts = 10)
{
while (true)
{
try
{
action();
return;
}
catch (Exception)
{
if (attempts-- == 0)
throw;
}
Thread.Sleep(250);
}
}
}

View File

@ -10,6 +10,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Threading;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
@ -150,9 +151,9 @@ namespace osu.Game.Online.Leaderboards
switch (placeholderState = value)
{
case PlaceholderState.NetworkFailure:
replacePlaceholder(new RetrievalFailurePlaceholder
replacePlaceholder(new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync)
{
OnRetry = UpdateScores,
Action = UpdateScores,
});
break;

View File

@ -1,65 +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;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Online.Placeholders;
using osuTK;
namespace osu.Game.Online.Leaderboards
{
public class RetrievalFailurePlaceholder : Placeholder
{
public Action OnRetry;
public RetrievalFailurePlaceholder()
{
AddArbitraryDrawable(new RetryButton
{
Action = () => OnRetry?.Invoke(),
Padding = new MarginPadding { Right = 10 }
});
AddText(@"Couldn't retrieve scores!");
}
public class RetryButton : OsuHoverContainer
{
private readonly SpriteIcon icon;
public new Action Action;
public RetryButton()
{
AutoSizeAxes = Axes.Both;
Child = new OsuClickableContainer
{
AutoSizeAxes = Axes.Both,
Action = () => Action?.Invoke(),
Child = icon = new SpriteIcon
{
Icon = FontAwesome.Solid.Sync,
Size = new Vector2(TEXT_SIZE),
Shadow = true,
},
};
}
protected override bool OnMouseDown(MouseDownEvent e)
{
icon.ScaleTo(0.8f, 4000, Easing.OutQuint);
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
icon.ScaleTo(1, 1000, Easing.OutElastic);
base.OnMouseUp(e);
}
}
}
}

View File

@ -103,6 +103,20 @@ namespace osu.Game.Online.Multiplayer
[JsonIgnore]
public readonly Bindable<int> Position = new Bindable<int>(-1);
/// <summary>
/// Create a copy of this room without online information.
/// Should be used to create a local copy of a room for submitting in the future.
/// </summary>
public Room CreateCopy()
{
var copy = new Room();
copy.CopyFrom(this);
copy.RoomID.Value = null;
return copy;
}
public void CopyFrom(Room other)
{
RoomID.Value = other.RoomID.Value;

View File

@ -0,0 +1,38 @@
// 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 osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Online.Placeholders
{
public class ClickablePlaceholder : Placeholder
{
public Action Action;
public ClickablePlaceholder(string actionMessage, IconUsage icon)
{
OsuTextFlowContainer textFlow;
AddArbitraryDrawable(new OsuAnimatedButton
{
AutoSizeAxes = Framework.Graphics.Axes.Both,
Child = textFlow = new OsuTextFlowContainer(cp => cp.Font = cp.Font.With(size: TEXT_SIZE))
{
AutoSizeAxes = Framework.Graphics.Axes.Both,
Margin = new Framework.Graphics.MarginPadding(5)
},
Action = () => Action?.Invoke()
});
textFlow.AddIcon(icon, i =>
{
i.Padding = new Framework.Graphics.MarginPadding { Right = 10 };
});
textFlow.AddText(actionMessage);
}
}
}

View File

@ -2,45 +2,20 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Overlays;
namespace osu.Game.Online.Placeholders
{
public sealed class LoginPlaceholder : Placeholder
public sealed class LoginPlaceholder : ClickablePlaceholder
{
[Resolved(CanBeNull = true)]
private LoginOverlay login { get; set; }
public LoginPlaceholder(string actionMessage)
: base(actionMessage, FontAwesome.Solid.UserLock)
{
AddIcon(FontAwesome.Solid.UserLock, cp =>
{
cp.Font = cp.Font.With(size: TEXT_SIZE);
cp.Padding = new MarginPadding { Right = 10 };
});
AddText(actionMessage);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
this.ScaleTo(0.8f, 4000, Easing.OutQuint);
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
this.ScaleTo(1, 1000, Easing.OutElastic);
base.OnMouseUp(e);
}
protected override bool OnClick(ClickEvent e)
{
login?.Show();
return base.OnClick(e);
Action = () => login?.Show();
}
}
}

View File

@ -406,7 +406,7 @@ namespace osu.Game
public void Migrate(string path)
{
contextFactory.FlushConnections();
(Storage as OsuStorage)?.Migrate(path);
(Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
}
}
}

View File

@ -76,7 +76,8 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
new SettingsEnumDropdown<ScoringMode>
{
LabelText = "Score display mode",
Current = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode)
Current = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode),
Keywords = new[] { "scoring" }
}
};

View File

@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Scoring
private readonly double accuracyPortion;
private readonly double comboPortion;
private int maxHighestCombo;
private int maxAchievableCombo;
private double maxBaseScore;
private double rollingMaxBaseScore;
private double baseScore;
@ -195,9 +195,9 @@ namespace osu.Game.Rulesets.Scoring
private double getScore(ScoringMode mode)
{
return GetScore(mode, maxHighestCombo,
return GetScore(mode, maxAchievableCombo,
maxBaseScore > 0 ? baseScore / maxBaseScore : 0,
maxHighestCombo > 0 ? (double)HighestCombo.Value / maxHighestCombo : 0,
maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1,
scoreResultCounts);
}
@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Scoring
case ScoringMode.Classic:
// should emulate osu-stable's scoring as closely as we can (https://osu.ppy.sh/help/wiki/Score/ScoreV1)
return getBonusScore(statistics) + (accuracyRatio * maxCombo * 300) * (1 + Math.Max(0, (comboRatio * maxCombo) - 1) * scoreMultiplier / 25);
return getBonusScore(statistics) + (accuracyRatio * Math.Max(1, maxCombo) * 300) * (1 + Math.Max(0, (comboRatio * maxCombo) - 1) * scoreMultiplier / 25);
}
}
@ -265,14 +265,8 @@ namespace osu.Game.Rulesets.Scoring
if (storeResults)
{
maxHighestCombo = HighestCombo.Value;
maxAchievableCombo = HighestCombo.Value;
maxBaseScore = baseScore;
if (maxBaseScore == 0 || maxHighestCombo == 0)
{
Mode.Value = ScoringMode.Classic;
Mode.Disabled = true;
}
}
baseScore = 0;

View File

@ -81,7 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
waveform = new WaveformGraph
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Blue.Opacity(0.2f),
BaseColour = colours.Blue.Opacity(0.2f),
LowColour = colours.BlueLighter,
MidColour = colours.BlueDark,
HighColour = colours.BlueDarker,

View File

@ -21,10 +21,12 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Components;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
namespace osu.Game.Screens.Multi.Lounge.Components
{
public class DrawableRoom : OsuClickableContainer, IStateful<SelectionState>, IFilterable
public class DrawableRoom : OsuClickableContainer, IStateful<SelectionState>, IFilterable, IHasContextMenu
{
public const float SELECTION_BORDER_WIDTH = 4;
private const float corner_radius = 5;
@ -39,6 +41,9 @@ namespace osu.Game.Screens.Multi.Lounge.Components
private readonly Box selectionBox;
private CachedModelDependencyContainer<Room> dependencies;
[Resolved(canBeNull: true)]
private Multiplayer multiplayer { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
@ -232,5 +237,13 @@ namespace osu.Game.Screens.Multi.Lounge.Components
Current = name;
}
}
public MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
{
multiplayer?.CreateRoom(Room.CreateCopy());
})
};
}
}

View File

@ -17,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Online.Multiplayer;
using osuTK;
using osu.Game.Graphics.Cursor;
namespace osu.Game.Screens.Multi.Lounge.Components
{
@ -38,17 +39,25 @@ namespace osu.Game.Screens.Multi.Lounge.Components
[Resolved]
private IRoomManager roomManager { get; set; }
[Resolved(CanBeNull = true)]
private LoungeSubScreen loungeSubScreen { get; set; }
public RoomsContainer()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = roomFlow = new FillFlowContainer<DrawableRoom>
InternalChild = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2),
Child = roomFlow = new FillFlowContainer<DrawableRoom>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2),
}
};
}

View File

@ -17,6 +17,7 @@ using osu.Game.Screens.Multi.Match;
namespace osu.Game.Screens.Multi.Lounge
{
[Cached]
public class LoungeSubScreen : MultiplayerSubScreen
{
public override string Title => "Lounge";
@ -125,7 +126,7 @@ namespace osu.Game.Screens.Multi.Lounge
if (selectedRoom.Value?.RoomID.Value == null)
selectedRoom.Value = new Room();
music.EnsurePlayingSomething();
music?.EnsurePlayingSomething();
onReturning();
}

View File

@ -51,7 +51,7 @@ namespace osu.Game.Screens.Multi
[Cached]
private readonly Bindable<FilterCriteria> currentFilter = new Bindable<FilterCriteria>(new FilterCriteria());
[Resolved]
[Resolved(CanBeNull = true)]
private MusicController music { get; set; }
[Cached(Type = typeof(IRoomManager))]
@ -134,7 +134,7 @@ namespace osu.Game.Screens.Multi
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Action = createRoom
Action = () => CreateRoom()
},
roomManager = new RoomManager()
}
@ -289,10 +289,11 @@ namespace osu.Game.Screens.Multi
logo.Delay(WaveContainer.DISAPPEAR_DURATION / 2).FadeOut();
}
private void createRoom()
{
loungeSubScreen.Open(new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } });
}
/// <summary>
/// Create a new room.
/// </summary>
/// <param name="room">An optional template to use when creating the room.</param>
public void CreateRoom(Room room = null) => loungeSubScreen.Open(room ?? new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } });
private void beginHandlingTrack()
{
@ -350,7 +351,7 @@ namespace osu.Game.Screens.Multi
track.RestartPoint = Beatmap.Value.Metadata.PreviewTime;
track.Looping = true;
music.EnsurePlayingSomething();
music?.EnsurePlayingSomething();
}
}
else

View File

@ -15,8 +15,6 @@ namespace osu.Game.Screens.Play.HUD
{
private readonly Vector2 offset = new Vector2(20, 5);
protected override double RollingDuration => 750;
[Resolved(canBeNull: true)]
private HUDOverlay hud { get; set; }

View File

@ -15,5 +15,11 @@ namespace osu.Game.Screens.Play.HUD
/// The current score to be displayed.
/// </summary>
Bindable<double> Current { get; }
/// <summary>
/// The number of digits required to display most sane scores.
/// This may be exceeded in very rare cases, but is useful to pad or space the display to avoid it jumping around.
/// </summary>
Bindable<int> RequiredDisplayDigits { get; }
}
}

View File

@ -1,7 +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.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD
@ -10,12 +14,38 @@ namespace osu.Game.Screens.Play.HUD
{
public Bindable<double> Current { get; } = new Bindable<double>();
private Bindable<ScoringMode> scoreDisplayMode;
public Bindable<int> RequiredDisplayDigits { get; } = new Bindable<int>();
public SkinnableScoreCounter()
: base(new HUDSkinComponent(HUDSkinComponents.ScoreCounter), _ => new DefaultScoreCounter())
{
CentreComponent = false;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
scoreDisplayMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
scoreDisplayMode.BindValueChanged(scoreMode =>
{
switch (scoreMode.NewValue)
{
case ScoringMode.Standardised:
RequiredDisplayDigits.Value = 6;
break;
case ScoringMode.Classic:
RequiredDisplayDigits.Value = 8;
break;
default:
throw new ArgumentOutOfRangeException(nameof(scoreMode));
}
}, true);
}
private IScoreCounter skinnedCounter;
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
@ -23,7 +53,9 @@ namespace osu.Game.Screens.Play.HUD
base.SkinChanged(skin, allowFallback);
skinnedCounter = Drawable as IScoreCounter;
skinnedCounter?.Current.BindTo(Current);
skinnedCounter?.RequiredDisplayDigits.BindTo(RequiredDisplayDigits);
}
}
}

View File

@ -24,7 +24,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1009.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1019.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1016.0" />
<PackageReference Include="Sentry" Version="2.1.6" />
<PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1009.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1019.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1016.0" />
</ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
@ -80,7 +80,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1009.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1019.0" />
<PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />