mirror of
https://github.com/ppy/osu
synced 2025-01-12 09:09:44 +00:00
Merge branch 'master' into fix-legacy-score-display-fixed-width
This commit is contained in:
commit
534088c66b
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
72
osu.Game.Tournament/IO/TournamentStorage.cs
Normal file
72
osu.Game.Tournament/IO/TournamentStorage.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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 =>
|
||||
|
132
osu.Game/IO/MigratableStorage.cs
Normal file
132
osu.Game/IO/MigratableStorage.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
38
osu.Game/Online/Placeholders/ClickablePlaceholder.cs
Normal file
38
osu.Game/Online/Placeholders/ClickablePlaceholder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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" }
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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());
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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" />
|
||||
|
@ -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" />
|
||||
|
Loading…
Reference in New Issue
Block a user