1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-12 05:07:22 +08:00

Merge branch 'master' into multi-queueing-modes

This commit is contained in:
Dan Balasescu 2021-11-11 22:19:30 +09:00
commit 07a7b4bcdc
74 changed files with 1184 additions and 277 deletions

View File

@ -52,22 +52,29 @@ jobs:
build-only-android: build-only-android:
name: Build only (Android) name: Build only (Android)
runs-on: windows-latest runs-on: macos-latest
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
# Pin Xamarin.Android version to 11.2 for now to avoid build failures caused by a Xamarin-side regression.
# See: https://github.com/xamarin/xamarin-android/issues/6284
# This can be removed/reverted when the fix makes it to upstream and is deployed on github runners.
- name: Set default Xamarin SDK version
run: |
$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.2
- name: Install .NET 5.0.x - name: Install .NET 5.0.x
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: "5.0.x" dotnet-version: "5.0.x"
- name: Setup MSBuild # Contrary to seemingly any other msbuild, msbuild running on macOS/Mono
uses: microsoft/setup-msbuild@v1 # cannot accept .sln(f) files as arguments.
# Build just the main game for now.
- name: Build - name: Build
run: msbuild osu.Android.slnf /restore /p:Configuration=Debug run: msbuild osu.Android/osu.Android.csproj /restore /p:Configuration=Debug
build-only-ios: build-only-ios:
# While this workflow technically *can* run, it fails as iOS builds are blocked by multiple issues. # While this workflow technically *can* run, it fails as iOS builds are blocked by multiple issues.

View File

@ -0,0 +1,118 @@
// 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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit.Checks;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests.Editor.Checks
{
[TestFixture]
public class TestCheckBananaShowerGap
{
private CheckBananaShowerGap check;
[SetUp]
public void Setup()
{
check = new CheckBananaShowerGap();
}
[Test]
public void TestAllowedSpinnerGaps()
{
assertOk(mockBeatmap(250, 1000, 1250), DifficultyRating.Easy);
assertOk(mockBeatmap(250, 1000, 1250), DifficultyRating.Normal);
assertOk(mockBeatmap(125, 1000, 1250), DifficultyRating.Hard);
assertOk(mockBeatmap(125, 1000, 1125), DifficultyRating.Insane);
assertOk(mockBeatmap(62, 1000, 1125), DifficultyRating.Expert);
assertOk(mockBeatmap(62, 1000, 1125), DifficultyRating.ExpertPlus);
}
[Test]
public void TestDisallowedSpinnerGapStart()
{
assertTooShortSpinnerStart(mockBeatmap(249, 1000, 1250), DifficultyRating.Easy);
assertTooShortSpinnerStart(mockBeatmap(249, 1000, 1250), DifficultyRating.Normal);
assertTooShortSpinnerStart(mockBeatmap(124, 1000, 1250), DifficultyRating.Hard);
assertTooShortSpinnerStart(mockBeatmap(124, 1000, 1250), DifficultyRating.Insane);
assertTooShortSpinnerStart(mockBeatmap(61, 1000, 1250), DifficultyRating.Expert);
assertTooShortSpinnerStart(mockBeatmap(61, 1000, 1250), DifficultyRating.ExpertPlus);
}
[Test]
public void TestDisallowedSpinnerGapEnd()
{
assertTooShortSpinnerEnd(mockBeatmap(250, 1000, 1249), DifficultyRating.Easy);
assertTooShortSpinnerEnd(mockBeatmap(250, 1000, 1249), DifficultyRating.Normal);
assertTooShortSpinnerEnd(mockBeatmap(125, 1000, 1249), DifficultyRating.Hard);
assertTooShortSpinnerEnd(mockBeatmap(125, 1000, 1124), DifficultyRating.Insane);
assertTooShortSpinnerEnd(mockBeatmap(62, 1000, 1124), DifficultyRating.Expert);
assertTooShortSpinnerEnd(mockBeatmap(62, 1000, 1124), DifficultyRating.ExpertPlus);
}
[Test]
public void TestConsecutiveSpinners()
{
var spinnerConsecutiveBeatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new BananaShower { StartTime = 0, EndTime = 100, X = 0 },
new BananaShower { StartTime = 101, EndTime = 200, X = 0 },
new BananaShower { StartTime = 201, EndTime = 300, X = 0 }
}
};
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Easy);
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Normal);
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Hard);
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Insane);
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Expert);
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.ExpertPlus);
}
private Beatmap<HitObject> mockBeatmap(double bananaShowerStart, double bananaShowerEnd, double nextFruitStart)
{
return new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new Fruit { StartTime = 0, X = 0 },
new BananaShower { StartTime = bananaShowerStart, EndTime = bananaShowerEnd, X = 0 },
new Fruit { StartTime = nextFruitStart, X = 0 }
}
};
}
private void assertOk(IBeatmap beatmap, DifficultyRating difficultyRating)
{
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
Assert.That(check.Run(context), Is.Empty);
}
private void assertTooShortSpinnerStart(IBeatmap beatmap, DifficultyRating difficultyRating)
{
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.All(issue => issue.Template is CheckBananaShowerGap.IssueTemplateBananaShowerStartGap));
}
private void assertTooShortSpinnerEnd(IBeatmap beatmap, DifficultyRating difficultyRating)
{
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.All(issue => issue.Template is CheckBananaShowerGap.IssueTemplateBananaShowerEndGap));
}
}
}

View File

@ -0,0 +1,110 @@
// 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.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests.Mods
{
public class TestSceneCatchModNoScope : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
[Test]
public void TestVisibleDuringBreak()
{
CreateModTest(new ModTestData
{
Mod = new CatchModNoScope
{
HiddenComboCount = { Value = 0 },
},
Autoplay = true,
PassCondition = () => true,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Fruit
{
X = CatchPlayfield.CENTER_X,
StartTime = 1000,
},
new Fruit
{
X = CatchPlayfield.CENTER_X,
StartTime = 5000,
}
},
Breaks = new List<BreakPeriod>
{
new BreakPeriod(2000, 4000),
}
}
});
AddUntilStep("wait for catcher to hide", () => catcherAlphaAlmostEquals(0));
AddUntilStep("wait for start of break", isBreak);
AddUntilStep("wait for catcher to show", () => catcherAlphaAlmostEquals(1));
AddUntilStep("wait for end of break", () => !isBreak());
AddUntilStep("wait for catcher to hide", () => catcherAlphaAlmostEquals(0));
}
[Test]
public void TestVisibleAfterComboBreak()
{
CreateModTest(new ModTestData
{
Mod = new CatchModNoScope
{
HiddenComboCount = { Value = 2 },
},
Autoplay = true,
PassCondition = () => true,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Fruit
{
X = 0,
StartTime = 1000,
},
new Fruit
{
X = CatchPlayfield.CENTER_X,
StartTime = 3000,
},
new Fruit
{
X = CatchPlayfield.WIDTH,
StartTime = 5000,
},
}
}
});
AddAssert("catcher must start visible", () => catcherAlphaAlmostEquals(1));
AddUntilStep("wait for combo", () => Player.ScoreProcessor.Combo.Value >= 2);
AddAssert("catcher must dim after combo", () => !catcherAlphaAlmostEquals(1));
AddStep("break combo", () => Player.ScoreProcessor.Combo.Value = 0);
AddUntilStep("wait for catcher to show", () => catcherAlphaAlmostEquals(1));
}
private bool isBreak() => Player.IsBreakTime.Value;
private bool catcherAlphaAlmostEquals(float alpha)
{
var playfield = (CatchPlayfield)Player.DrawableRuleset.Playfield;
return Precision.AlmostEquals(playfield.CatcherArea.Alpha, alpha);
}
}
}

View File

@ -133,6 +133,7 @@ namespace osu.Game.Rulesets.Catch
new MultiMod(new ModWindUp(), new ModWindDown()), new MultiMod(new ModWindUp(), new ModWindDown()),
new CatchModFloatingFruits(), new CatchModFloatingFruits(),
new CatchModMuted(), new CatchModMuted(),
new CatchModNoScope(),
}; };
default: default:
@ -188,5 +189,7 @@ namespace osu.Game.Rulesets.Catch
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this); public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
} }
} }

View File

@ -0,0 +1,24 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Catch.Edit.Checks;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Catch.Edit
{
public class CatchBeatmapVerifier : IBeatmapVerifier
{
private readonly List<ICheck> checks = new List<ICheck>
{
new CheckBananaShowerGap()
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
return checks.SelectMany(check => check.Run(context));
}
}
}

View File

@ -0,0 +1,102 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Catch.Edit.Checks
{
/// <summary>
/// Check the spinner/banana shower gaps specified in the osu!catch difficulty specific ranking criteria.
/// </summary>
public class CheckBananaShowerGap : ICheck
{
private static readonly Dictionary<DifficultyRating, (int startGap, int endGap)> spinner_delta_threshold = new Dictionary<DifficultyRating, (int, int)>
{
[DifficultyRating.Easy] = (250, 250),
[DifficultyRating.Normal] = (250, 250),
[DifficultyRating.Hard] = (125, 250),
[DifficultyRating.Insane] = (125, 125),
[DifficultyRating.Expert] = (62, 125),
[DifficultyRating.ExpertPlus] = (62, 125)
};
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Too short spinner gap");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateBananaShowerStartGap(this),
new IssueTemplateBananaShowerEndGap(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var hitObjects = context.Beatmap.HitObjects;
(int expectedStartDelta, int expectedEndDelta) = spinner_delta_threshold[context.InterpretedDifficulty];
for (int i = 0; i < hitObjects.Count - 1; ++i)
{
if (!(hitObjects[i] is BananaShower bananaShower))
continue;
// Skip if the previous hitobject is a banana shower, consecutive spinners are allowed
if (i != 0 && hitObjects[i - 1] is CatchHitObject previousHitObject && !(previousHitObject is BananaShower))
{
double spinnerStartDelta = bananaShower.StartTime - previousHitObject.GetEndTime();
if (spinnerStartDelta < expectedStartDelta)
{
yield return new IssueTemplateBananaShowerStartGap(this)
.Create(spinnerStartDelta, expectedStartDelta, bananaShower, previousHitObject);
}
}
// Skip if the next hitobject is a banana shower, consecutive spinners are allowed
if (hitObjects[i + 1] is CatchHitObject nextHitObject && !(nextHitObject is BananaShower))
{
double spinnerEndDelta = nextHitObject.StartTime - bananaShower.EndTime;
if (spinnerEndDelta < expectedEndDelta)
{
yield return new IssueTemplateBananaShowerEndGap(this)
.Create(spinnerEndDelta, expectedEndDelta, bananaShower, nextHitObject);
}
}
}
}
public abstract class IssueTemplateBananaShowerGap : IssueTemplate
{
protected IssueTemplateBananaShowerGap(ICheck check, IssueType issueType, string unformattedMessage)
: base(check, issueType, unformattedMessage)
{
}
public Issue Create(double deltaTime, int expectedDeltaTime, params HitObject[] hitObjects)
{
return new Issue(hitObjects, this, Math.Floor(deltaTime), expectedDeltaTime);
}
}
public class IssueTemplateBananaShowerStartGap : IssueTemplateBananaShowerGap
{
public IssueTemplateBananaShowerStartGap(ICheck check)
: base(check, IssueType.Problem, "There is only {0} ms between the start of the spinner and the last object, it should not be less than {1} ms.")
{
}
}
public class IssueTemplateBananaShowerEndGap : IssueTemplateBananaShowerGap
{
public IssueTemplateBananaShowerEndGap(ICheck check)
: base(check, IssueType.Problem, "There is only {0} ms between the end of the spinner and the next object, it should not be less than {1} ms.")
{
}
}
}
}

View File

@ -0,0 +1,40 @@
// 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.Bindables;
using osu.Game.Rulesets.Mods;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModNoScope : ModNoScope, IUpdatableByPlayfield
{
public override string Description => "Where's the catcher?";
[SettingSource(
"Hidden at combo",
"The combo count at which the catcher becomes completely hidden",
SettingControlType = typeof(SettingsSlider<int, HiddenComboSlider>)
)]
public override BindableInt HiddenComboCount { get; } = new BindableInt
{
Default = 10,
Value = 10,
MinValue = 0,
MaxValue = 50,
};
public void Update(Playfield playfield)
{
var catchPlayfield = (CatchPlayfield)playfield;
bool shouldAlwaysShowCatcher = IsBreakTime.Value;
float targetAlpha = shouldAlwaysShowCatcher ? 1 : ComboBasedAlpha;
catchPlayfield.CatcherArea.Alpha = (float)Interpolation.Lerp(catchPlayfield.CatcherArea.Alpha, targetAlpha, Math.Clamp(catchPlayfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
}
}
}

View File

@ -22,6 +22,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Resolved] [Resolved]
private SkinManager skins { get; set; } private SkinManager skins { get; set; }
[Cached]
private EditorClipboard clipboard = new EditorClipboard();
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {

View File

@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
AddAssert("cursor must start visible", () => cursorAlphaAlmostEquals(1)); AddAssert("cursor must start visible", () => cursorAlphaAlmostEquals(1));
AddUntilStep("wait for combo", () => Player.ScoreProcessor.Combo.Value >= 2); AddUntilStep("wait for combo", () => Player.ScoreProcessor.Combo.Value >= 2);
AddAssert("cursor must dim after combo", () => !cursorAlphaAlmostEquals(1)); AddAssert("cursor must dim after combo", () => !cursorAlphaAlmostEquals(1));
AddStep("break combo", () => Player.ScoreProcessor.Combo.Set(0)); AddStep("break combo", () => Player.ScoreProcessor.Combo.Value = 0);
AddUntilStep("wait for cursor to show", () => cursorAlphaAlmostEquals(1)); AddUntilStep("wait for cursor to show", () => cursorAlphaAlmostEquals(1));
} }

View File

@ -4,52 +4,29 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Utils; using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModNoScope : Mod, IUpdatableByPlayfield, IApplicableToScoreProcessor, IApplicableToPlayer, IApplicableToBeatmap public class OsuModNoScope : ModNoScope, IUpdatableByPlayfield, IApplicableToBeatmap
{ {
/// <summary>
/// Slightly higher than the cutoff for <see cref="Drawable.IsPresent"/>.
/// </summary>
private const float min_alpha = 0.0002f;
private const float transition_duration = 100;
public override string Name => "No Scope";
public override string Acronym => "NS";
public override ModType Type => ModType.Fun;
public override IconUsage? Icon => FontAwesome.Solid.EyeSlash;
public override string Description => "Where's the cursor?"; public override string Description => "Where's the cursor?";
public override double ScoreMultiplier => 1;
private BindableNumber<int> currentCombo;
private IBindable<bool> isBreakTime;
private PeriodTracker spinnerPeriods; private PeriodTracker spinnerPeriods;
private float comboBasedAlpha;
[SettingSource( [SettingSource(
"Hidden at combo", "Hidden at combo",
"The combo count at which the cursor becomes completely hidden", "The combo count at which the cursor becomes completely hidden",
SettingControlType = typeof(SettingsSlider<int, HiddenComboSlider>) SettingControlType = typeof(SettingsSlider<int, HiddenComboSlider>)
)] )]
public BindableInt HiddenComboCount { get; } = new BindableInt public override BindableInt HiddenComboCount { get; } = new BindableInt
{ {
Default = 10, Default = 10,
Value = 10, Value = 10,
@ -57,39 +34,16 @@ namespace osu.Game.Rulesets.Osu.Mods
MaxValue = 50, MaxValue = 50,
}; };
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
public void ApplyToPlayer(Player player)
{
isBreakTime = player.IsBreakTime.GetBoundCopy();
}
public void ApplyToBeatmap(IBeatmap beatmap) public void ApplyToBeatmap(IBeatmap beatmap)
{ {
spinnerPeriods = new PeriodTracker(beatmap.HitObjects.OfType<Spinner>().Select(b => new Period(b.StartTime - transition_duration, b.EndTime))); spinnerPeriods = new PeriodTracker(beatmap.HitObjects.OfType<Spinner>().Select(b => new Period(b.StartTime - TRANSITION_DURATION, b.EndTime)));
} }
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) public void Update(Playfield playfield)
{ {
if (HiddenComboCount.Value == 0) return; bool shouldAlwaysShowCursor = IsBreakTime.Value || spinnerPeriods.IsInAny(playfield.Clock.CurrentTime);
float targetAlpha = shouldAlwaysShowCursor ? 1 : ComboBasedAlpha;
currentCombo = scoreProcessor.Combo.GetBoundCopy(); playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
currentCombo.BindValueChanged(combo =>
{
comboBasedAlpha = Math.Max(min_alpha, 1 - (float)combo.NewValue / HiddenComboCount.Value);
}, true);
}
public virtual void Update(Playfield playfield)
{
bool shouldAlwaysShowCursor = isBreakTime.Value || spinnerPeriods.IsInAny(playfield.Clock.CurrentTime);
float targetAlpha = shouldAlwaysShowCursor ? 1 : comboBasedAlpha;
playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / transition_duration, 0, 1));
} }
} }
public class HiddenComboSlider : OsuSliderBar<int>
{
public override LocalisableString TooltipText => Current.Value == 0 ? "always hidden" : base.TooltipText;
}
} }

View File

@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor
editorBeatmap.BeatmapInfo.Metadata.Artist = "artist"; editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
editorBeatmap.BeatmapInfo.Metadata.Title = "title"; editorBeatmap.BeatmapInfo.Metadata.Title = "title";
}); });
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.Version = "difficulty"); AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty");
checkMutations(); checkMutations();
@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor
return Precision.AlmostEquals(taikoDifficulty.SliderMultiplier, 2); return Precision.AlmostEquals(taikoDifficulty.SliderMultiplier, 2);
}); });
AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title"); AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title");
AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.Version == "difficulty"); AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
} }
} }
} }

View File

@ -113,7 +113,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual("Soleily", metadata.Artist); Assert.AreEqual("Soleily", metadata.Artist);
Assert.AreEqual("Soleily", metadata.ArtistUnicode); Assert.AreEqual("Soleily", metadata.ArtistUnicode);
Assert.AreEqual("Gamu", metadata.Author.Username); Assert.AreEqual("Gamu", metadata.Author.Username);
Assert.AreEqual("Insane", beatmapInfo.Version); Assert.AreEqual("Insane", beatmapInfo.DifficultyName);
Assert.AreEqual(string.Empty, metadata.Source); Assert.AreEqual(string.Empty, metadata.Source);
Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", metadata.Tags); Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", metadata.Tags);
Assert.AreEqual(557821, beatmapInfo.OnlineBeatmapID); Assert.AreEqual(557821, beatmapInfo.OnlineBeatmapID);

View File

@ -790,12 +790,12 @@ namespace osu.Game.Tests.Beatmaps.IO
// Update via the beatmap, not the beatmap info, to ensure correct linking // Update via the beatmap, not the beatmap info, to ensure correct linking
BeatmapSetInfo setToUpdate = manager.GetAllUsableBeatmapSets()[0]; BeatmapSetInfo setToUpdate = manager.GetAllUsableBeatmapSets()[0];
Beatmap beatmapToUpdate = (Beatmap)manager.GetWorkingBeatmap(setToUpdate.Beatmaps.First(b => b.RulesetID == 0)).Beatmap; Beatmap beatmapToUpdate = (Beatmap)manager.GetWorkingBeatmap(setToUpdate.Beatmaps.First(b => b.RulesetID == 0)).Beatmap;
beatmapToUpdate.BeatmapInfo.Version = "updated"; beatmapToUpdate.BeatmapInfo.DifficultyName = "updated";
manager.Update(setToUpdate); manager.Update(setToUpdate);
BeatmapInfo updatedInfo = manager.QueryBeatmap(b => b.ID == beatmapToUpdate.BeatmapInfo.ID); BeatmapInfo updatedInfo = manager.QueryBeatmap(b => b.ID == beatmapToUpdate.BeatmapInfo.ID);
Assert.That(updatedInfo.Version, Is.EqualTo("updated")); Assert.That(updatedInfo.DifficultyName, Is.EqualTo("updated"));
} }
finally finally
{ {
@ -863,7 +863,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var beatmap = working.Beatmap; var beatmap = working.Beatmap;
beatmap.BeatmapInfo.Version = "difficulty"; beatmap.BeatmapInfo.DifficultyName = "difficulty";
beatmap.BeatmapInfo.Metadata = new BeatmapMetadata beatmap.BeatmapInfo.Metadata = new BeatmapMetadata
{ {
Artist = "Artist/With\\Slashes", Artist = "Artist/With\\Slashes",

View File

@ -52,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps
Title = "title", Title = "title",
Author = new APIUser { Username = "creator" } Author = new APIUser { Username = "creator" }
}, },
Version = "difficulty" DifficultyName = "difficulty"
}; };
Assert.That(beatmap.ToString(), Is.EqualTo("artist - title (creator) [difficulty]")); Assert.That(beatmap.ToString(), Is.EqualTo("artist - title (creator) [difficulty]"));

View File

@ -54,7 +54,7 @@ namespace osu.Game.Tests.Editing.Checks
{ {
// While this is a problem, it is out of scope for this check and is caught by a different one. // While this is a problem, it is out of scope for this check and is caught by a different one.
beatmap.Metadata.BackgroundFile = string.Empty; beatmap.Metadata.BackgroundFile = string.Empty;
var context = getContext(null, System.Array.Empty<byte>()); var context = getContext(null, new MemoryStream(System.Array.Empty<byte>()));
Assert.That(check.Run(context), Is.Empty); Assert.That(check.Run(context), Is.Empty);
} }
@ -103,7 +103,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test] [Test]
public void TestTooUncompressed() public void TestTooUncompressed()
{ {
var context = getContext(new Texture(1920, 1080), new byte[1024 * 1024 * 3]); var context = getContext(new Texture(1920, 1080), new MemoryStream(new byte[1024 * 1024 * 3]));
var issues = check.Run(context).ToList(); var issues = check.Run(context).ToList();
@ -111,19 +111,32 @@ namespace osu.Game.Tests.Editing.Checks
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateTooUncompressed); Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateTooUncompressed);
} }
private BeatmapVerifierContext getContext(Texture background, [CanBeNull] byte[] fileBytes = null) [Test]
public void TestStreamClosed()
{ {
return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(background, fileBytes).Object); var background = new Texture(1920, 1080);
var stream = new Mock<MemoryStream>(new byte[1024 * 1024]);
var context = getContext(background, stream.Object);
Assert.That(check.Run(context), Is.Empty);
stream.Verify(x => x.Close(), Times.Once());
}
private BeatmapVerifierContext getContext(Texture background, [CanBeNull] Stream stream = null)
{
return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(background, stream).Object);
} }
/// <summary> /// <summary>
/// Returns the mock of the working beatmap with the given background and filesize. /// Returns the mock of the working beatmap with the given background and its file stream.
/// </summary> /// </summary>
/// <param name="background">The texture of the background.</param> /// <param name="background">The texture of the background.</param>
/// <param name="fileBytes">The bytes that represent the background file.</param> /// <param name="stream">The stream representing the background file.</param>
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(Texture background, [CanBeNull] byte[] fileBytes = null) private Mock<IWorkingBeatmap> getMockWorkingBeatmap(Texture background, [CanBeNull] Stream stream = null)
{ {
var stream = new MemoryStream(fileBytes ?? new byte[1024 * 1024]); stream ??= new MemoryStream(new byte[1024 * 1024]);
var mock = new Mock<IWorkingBeatmap>(); var mock = new Mock<IWorkingBeatmap>();
mock.SetupGet(w => w.Beatmap).Returns(beatmap); mock.SetupGet(w => w.Beatmap).Returns(beatmap);

View File

@ -0,0 +1,124 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Tests.Models
{
[TestFixture]
public class DisplayStringTest
{
[Test]
public void TestBeatmapSet()
{
var mock = new Mock<IBeatmapSetInfo>();
mock.Setup(m => m.Metadata.Artist).Returns("artist");
mock.Setup(m => m.Metadata.Title).Returns("title");
mock.Setup(m => m.Metadata.Author.Username).Returns("author");
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("artist - title (author)"));
}
[Test]
public void TestBeatmapSetWithNoAuthor()
{
var mock = new Mock<IBeatmapSetInfo>();
mock.Setup(m => m.Metadata.Artist).Returns("artist");
mock.Setup(m => m.Metadata.Title).Returns("title");
mock.Setup(m => m.Metadata.Author.Username).Returns(string.Empty);
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("artist - title"));
}
[Test]
public void TestBeatmapSetWithNoMetadata()
{
var mock = new Mock<IBeatmapSetInfo>();
mock.Setup(m => m.Metadata).Returns(new BeatmapMetadata());
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("unknown artist - unknown title"));
}
[Test]
public void TestBeatmap()
{
var mock = new Mock<IBeatmapInfo>();
mock.Setup(m => m.Metadata.Artist).Returns("artist");
mock.Setup(m => m.Metadata.Title).Returns("title");
mock.Setup(m => m.Metadata.Author.Username).Returns("author");
mock.Setup(m => m.DifficultyName).Returns("difficulty");
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("artist - title (author) [difficulty]"));
}
[Test]
public void TestMetadata()
{
var mock = new Mock<IBeatmapMetadataInfo>();
mock.Setup(m => m.Artist).Returns("artist");
mock.Setup(m => m.Title).Returns("title");
mock.Setup(m => m.Author.Username).Returns("author");
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("artist - title (author)"));
}
[Test]
public void TestScore()
{
var mock = new Mock<IScoreInfo>();
mock.Setup(m => m.User).Returns(new APIUser { Username = "user" }); // TODO: temporary.
mock.Setup(m => m.Beatmap.Metadata.Artist).Returns("artist");
mock.Setup(m => m.Beatmap.Metadata.Title).Returns("title");
mock.Setup(m => m.Beatmap.Metadata.Author.Username).Returns("author");
mock.Setup(m => m.Beatmap.DifficultyName).Returns("difficulty");
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("user playing artist - title (author) [difficulty]"));
}
[Test]
public void TestRuleset()
{
var mock = new Mock<IRulesetInfo>();
mock.Setup(m => m.Name).Returns("ruleset");
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("ruleset"));
}
[Test]
public void TestUser()
{
var mock = new Mock<IUser>();
mock.Setup(m => m.Username).Returns("user");
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("user"));
}
[Test]
public void TestFallback()
{
var fallback = new Fallback();
Assert.That(fallback.GetDisplayString(), Is.EqualTo("fallback"));
}
private class Fallback
{
public override string ToString() => "fallback";
}
}
}

View File

@ -17,7 +17,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
private BeatmapInfo getExampleBeatmap() => new BeatmapInfo private BeatmapInfo getExampleBeatmap() => new BeatmapInfo
{ {
Ruleset = new RulesetInfo { ID = 5 }, Ruleset = new RulesetInfo { ID = 5 },
StarDifficulty = 4.0d, StarRating = 4.0d,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
ApproachRate = 5.0f, ApproachRate = 5.0f,
@ -34,7 +34,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
Source = "unit tests", Source = "unit tests",
Tags = "look for tags too", Tags = "look for tags too",
}, },
Version = "version as well", DifficultyName = "version as well",
Length = 2500, Length = 2500,
BPM = 160, BPM = 160,
BeatDivisor = 12, BeatDivisor = 12,

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -16,7 +17,30 @@ namespace osu.Game.Tests.Online
private BeatmapManager beatmaps; private BeatmapManager beatmaps;
private ProgressNotification recentNotification; private ProgressNotification recentNotification;
private static readonly BeatmapSetInfo test_model = new BeatmapSetInfo { OnlineBeatmapSetID = 1 }; private static readonly BeatmapSetInfo test_db_model = new BeatmapSetInfo
{
OnlineBeatmapSetID = 1,
Metadata = new BeatmapMetadata
{
Artist = "test author",
Title = "test title",
Author = new APIUser
{
Username = "mapper"
}
}
};
private static readonly APIBeatmapSet test_online_model = new APIBeatmapSet
{
OnlineID = 2,
Artist = "test author",
Title = "test title",
Author = new APIUser
{
Username = "mapper"
}
};
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(BeatmapManager beatmaps) private void load(BeatmapManager beatmaps)
@ -26,25 +50,41 @@ namespace osu.Game.Tests.Online
beatmaps.PostNotification = n => recentNotification = n as ProgressNotification; beatmaps.PostNotification = n => recentNotification = n as ProgressNotification;
} }
private static readonly object[][] notification_test_cases =
{
new object[] { test_db_model },
new object[] { test_online_model }
};
[TestCaseSource(nameof(notification_test_cases))]
public void TestNotificationMessage(IBeatmapSetInfo model)
{
AddStep("clear recent notification", () => recentNotification = null);
AddStep("download beatmap", () => beatmaps.Download(model));
AddUntilStep("wait for notification", () => recentNotification != null);
AddUntilStep("notification text correct", () => recentNotification.Text.ToString() == "Downloading test author - test title (mapper)");
}
[Test] [Test]
public void TestCancelDownloadFromRequest() public void TestCancelDownloadFromRequest()
{ {
AddStep("download beatmap", () => beatmaps.Download(test_model)); AddStep("download beatmap", () => beatmaps.Download(test_db_model));
AddStep("cancel download from request", () => beatmaps.GetExistingDownload(test_model).Cancel()); AddStep("cancel download from request", () => beatmaps.GetExistingDownload(test_db_model).Cancel());
AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_model) == null); AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_db_model) == null);
AddAssert("is notification cancelled", () => recentNotification.State == ProgressNotificationState.Cancelled); AddAssert("is notification cancelled", () => recentNotification.State == ProgressNotificationState.Cancelled);
} }
[Test] [Test]
public void TestCancelDownloadFromNotification() public void TestCancelDownloadFromNotification()
{ {
AddStep("download beatmap", () => beatmaps.Download(test_model)); AddStep("download beatmap", () => beatmaps.Download(test_db_model));
AddStep("cancel download from notification", () => recentNotification.Close()); AddStep("cancel download from notification", () => recentNotification.Close());
AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_model) == null); AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_db_model) == null);
AddAssert("is notification cancelled", () => recentNotification.State == ProgressNotificationState.Cancelled); AddAssert("is notification cancelled", () => recentNotification.State == ProgressNotificationState.Cancelled);
} }
} }

View File

@ -26,6 +26,9 @@ namespace osu.Game.Tests.Visual.Editing
} }
}); });
[Cached]
private EditorClipboard clipboard = new EditorClipboard();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {

View File

@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Editing
editorBeatmap.BeatmapInfo.Metadata.Artist = "artist"; editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
editorBeatmap.BeatmapInfo.Metadata.Title = "title"; editorBeatmap.BeatmapInfo.Metadata.Title = "title";
}); });
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.Version = "difficulty"); AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty");
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1); AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7); AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7);
AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title"); AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title");
AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.Version == "difficulty"); AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
} }
} }
} }

View File

@ -175,7 +175,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Id = 39828, Id = 39828,
Username = @"WubWoofWolf", Username = @"WubWoofWolf",
} }
}.CreateScoreInfo(rulesets); }.CreateScoreInfo(rulesets, CreateBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo);
} }
private class TestReplayDownloadButton : ReplayDownloadButton private class TestReplayDownloadButton : ReplayDownloadButton

View File

@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
BeatmapInfo = BeatmapInfo =
{ {
StarDifficulty = 2.5 StarRating = 2.5
} }
}.BeatmapInfo, }.BeatmapInfo,
} }
@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
BeatmapInfo = BeatmapInfo =
{ {
StarDifficulty = 2.5, StarRating = 2.5,
Metadata = Metadata =
{ {
Artist = "very very very very very very very very very long artist", Artist = "very very very very very very very very very long artist",
@ -111,7 +111,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
BeatmapInfo = BeatmapInfo =
{ {
StarDifficulty = 2.5 StarRating = 2.5
} }
}.BeatmapInfo, }.BeatmapInfo,
} }
@ -124,7 +124,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
BeatmapInfo = BeatmapInfo =
{ {
StarDifficulty = 4.5 StarRating = 4.5
} }
}.BeatmapInfo, }.BeatmapInfo,
} }

View File

@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
Ruleset = new OsuRuleset().RulesetInfo, Ruleset = new OsuRuleset().RulesetInfo,
OnlineBeatmapID = beatmapId, OnlineBeatmapID = beatmapId,
Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})", DifficultyName = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
Length = length, Length = length,
BPM = bpm, BPM = bpm,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty

View File

@ -31,8 +31,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
SelectedRoom.Value.Playlist.AddRange(new[] SelectedRoom.Value.Playlist.AddRange(new[]
{ {
new PlaylistItem { Beatmap = { Value = new BeatmapInfo { StarDifficulty = min } } }, new PlaylistItem { Beatmap = { Value = new BeatmapInfo { StarRating = min } } },
new PlaylistItem { Beatmap = { Value = new BeatmapInfo { StarDifficulty = max } } }, new PlaylistItem { Beatmap = { Value = new BeatmapInfo { StarRating = max } } },
}); });
}); });
} }

View File

@ -10,6 +10,7 @@ using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -97,16 +98,24 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
AddAssert("user on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); AddAssert("user on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
AddStep("add another user", () => client.AddUser(new APIUser { Username = "otheruser", Id = 44 }));
AddStep("press button", () => AddStep("press own button", () =>
{ {
InputManager.MoveMouseTo(multiplayerScreenStack.ChildrenOfType<TeamDisplay>().First()); InputManager.MoveMouseTo(multiplayerScreenStack.ChildrenOfType<TeamDisplay>().First());
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddAssert("user on team 1", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1); AddAssert("user on team 1", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1);
AddStep("press button", () => InputManager.Click(MouseButton.Left)); AddStep("press own button again", () => InputManager.Click(MouseButton.Left));
AddAssert("user on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0); AddAssert("user on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
AddStep("press other user's button", () =>
{
InputManager.MoveMouseTo(multiplayerScreenStack.ChildrenOfType<TeamDisplay>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddAssert("user still on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
} }
[Test] [Test]
@ -127,9 +136,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead); AddAssert("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead);
AddUntilStep("team displays are not displaying teams", () => multiplayerScreenStack.ChildrenOfType<TeamDisplay>().All(d => d.DisplayedTeam == null));
AddStep("change to team vs", () => client.ChangeSettings(matchType: MatchType.TeamVersus)); AddStep("change to team vs", () => client.ChangeSettings(matchType: MatchType.TeamVersus));
AddAssert("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus); AddAssert("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
AddUntilStep("team displays are displaying teams", () => multiplayerScreenStack.ChildrenOfType<TeamDisplay>().All(d => d.DisplayedTeam != null));
} }
private void createRoom(Func<Room> room) private void createRoom(Func<Room> room)

View File

@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.SongSelect
OverallDifficulty = 5.7f, OverallDifficulty = 5.7f,
ApproachRate = 3.5f ApproachRate = 3.5f
}, },
StarDifficulty = 4.5f StarRating = 4.5f
}; };
[Test] [Test]
@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.SongSelect
OverallDifficulty = 4.5f, OverallDifficulty = 4.5f,
ApproachRate = 3.1f ApproachRate = 3.1f
}, },
StarDifficulty = 8 StarRating = 8
}); });
AddAssert("first bar text is Key Count", () => advancedStats.ChildrenOfType<SpriteText>().First().Text == "Key Count"); AddAssert("first bar text is Key Count", () => advancedStats.ChildrenOfType<SpriteText>().First().Text == "Key Count");

View File

@ -435,8 +435,8 @@ namespace osu.Game.Tests.Visual.SongSelect
for (int i = 0; i < 3; i++) for (int i = 0; i < 3; i++)
{ {
var set = createTestBeatmapSet(i); var set = createTestBeatmapSet(i);
set.Beatmaps[0].StarDifficulty = 3 - i; set.Beatmaps[0].StarRating = 3 - i;
set.Beatmaps[2].StarDifficulty = 6 + i; set.Beatmaps[2].StarRating = 6 + i;
sets.Add(set); sets.Add(set);
} }
@ -684,9 +684,9 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
set.Beatmaps.Add(new BeatmapInfo set.Beatmaps.Add(new BeatmapInfo
{ {
Version = $"Stars: {i}", DifficultyName = $"Stars: {i}",
Ruleset = new OsuRuleset().RulesetInfo, Ruleset = new OsuRuleset().RulesetInfo,
StarDifficulty = i, StarRating = i,
}); });
} }
@ -868,8 +868,8 @@ namespace osu.Game.Tests.Visual.SongSelect
yield return new BeatmapInfo yield return new BeatmapInfo
{ {
OnlineBeatmapID = id++ * 10, OnlineBeatmapID = id++ * 10,
Version = version, DifficultyName = version,
StarDifficulty = diff, StarRating = diff,
Ruleset = new OsuRuleset().RulesetInfo, Ruleset = new OsuRuleset().RulesetInfo,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
@ -902,9 +902,9 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
OnlineBeatmapID = b * 10, OnlineBeatmapID = b * 10,
Path = $"extra{b}.osu", Path = $"extra{b}.osu",
Version = $"Extra {b}", DifficultyName = $"Extra {b}",
Ruleset = rulesets.GetRuleset((b - 1) % 4), Ruleset = rulesets.GetRuleset((b - 1) % 4),
StarDifficulty = 2, StarRating = 2,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
OverallDifficulty = 3.5f, OverallDifficulty = 3.5f,

View File

@ -198,8 +198,8 @@ namespace osu.Game.Tests.Visual.SongSelect
Title = $"{ruleset.ShortName}Title" Title = $"{ruleset.ShortName}Title"
}, },
Ruleset = ruleset, Ruleset = ruleset,
StarDifficulty = 6, StarRating = 6,
Version = $"{ruleset.ShortName}Version", DifficultyName = $"{ruleset.ShortName}Version",
BaseDifficulty = new BeatmapDifficulty() BaseDifficulty = new BeatmapDifficulty()
}, },
HitObjects = objects HitObjects = objects
@ -219,7 +219,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Source = "Verrrrry long Source", Source = "Verrrrry long Source",
Title = "Verrrrry long Title" Title = "Verrrrry long Title"
}, },
Version = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version", DifficultyName = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version",
Status = BeatmapSetOnlineStatus.Graveyard, Status = BeatmapSetOnlineStatus.Graveyard,
}, },
}; };

View File

@ -45,8 +45,8 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Title = title, Title = title,
}, },
Version = version, DifficultyName = version,
StarDifficulty = RNG.NextDouble(0, 10), StarRating = RNG.NextDouble(0, 10),
} }
})); }));
} }
@ -64,8 +64,8 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Title = "Heavy beatmap", Title = "Heavy beatmap",
}, },
Version = "10k objects", DifficultyName = "10k objects",
StarDifficulty = 99.99f, StarRating = 99.99f,
} }
})); }));

View File

@ -188,8 +188,8 @@ namespace osu.Game.Tests.Visual.SongSelect
Metadata = metadata, Metadata = metadata,
BaseDifficulty = new BeatmapDifficulty(), BaseDifficulty = new BeatmapDifficulty(),
Ruleset = ruleset, Ruleset = ruleset,
StarDifficulty = difficultyIndex + 1, StarRating = difficultyIndex + 1,
Version = $"SR{difficultyIndex + 1}" DifficultyName = $"SR{difficultyIndex + 1}"
}).ToList() }).ToList()
}; };

View File

@ -919,7 +919,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Ruleset = getRuleset(), Ruleset = getRuleset(),
OnlineBeatmapID = beatmapId, OnlineBeatmapID = beatmapId,
Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})", DifficultyName = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
Length = length, Length = length,
BPM = bpm, BPM = bpm,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty

View File

@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Username = "TestAuthor" Username = "TestAuthor"
}, },
}, },
Version = "Insane" DifficultyName = "Insane"
}, },
} }
}, },

View File

@ -56,7 +56,7 @@ namespace osu.Game.Beatmaps
Title = @"Unknown", Title = @"Unknown",
AuthorString = @"Unknown Creator", AuthorString = @"Unknown Creator",
}, },
Version = @"Normal", DifficultyName = @"Normal",
BaseDifficulty = Difficulty, BaseDifficulty = Difficulty,
}; };
} }

View File

@ -134,12 +134,12 @@ namespace osu.Game.Beatmaps
public double TimelineZoom { get; set; } public double TimelineZoom { get; set; }
// Metadata // Metadata
public string Version { get; set; } [Column("Version")]
public string DifficultyName { get; set; }
private string versionString => string.IsNullOrEmpty(Version) ? string.Empty : $"[{Version}]";
[JsonProperty("difficulty_rating")] [JsonProperty("difficulty_rating")]
public double StarDifficulty { get; set; } [Column("StarDifficulty")]
public double StarRating { get; set; }
/// <summary> /// <summary>
/// Currently only populated for beatmap deletion. Use <see cref="ScoreManager"/> to query scores. /// Currently only populated for beatmap deletion. Use <see cref="ScoreManager"/> to query scores.
@ -147,7 +147,7 @@ namespace osu.Game.Beatmaps
public List<ScoreInfo> Scores { get; set; } public List<ScoreInfo> Scores { get; set; }
[JsonIgnore] [JsonIgnore]
public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarDifficulty); public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarRating);
public override string ToString() => this.GetDisplayTitle(); public override string ToString() => this.GetDisplayTitle();
@ -182,9 +182,6 @@ namespace osu.Game.Beatmaps
#region Implementation of IBeatmapInfo #region Implementation of IBeatmapInfo
[JsonIgnore]
string IBeatmapInfo.DifficultyName => Version;
[JsonIgnore] [JsonIgnore]
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata ?? BeatmapSet?.Metadata ?? new BeatmapMetadata(); IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata ?? BeatmapSet?.Metadata ?? new BeatmapMetadata();
@ -197,9 +194,6 @@ namespace osu.Game.Beatmaps
[JsonIgnore] [JsonIgnore]
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
[JsonIgnore]
double IBeatmapInfo.StarRating => StarDifficulty;
#endregion #endregion
} }
} }

View File

@ -11,7 +11,7 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// A user-presentable display title representing this beatmap. /// A user-presentable display title representing this beatmap.
/// </summary> /// </summary>
public static string GetDisplayTitle(this IBeatmapInfo beatmapInfo) => $"{beatmapInfo.Metadata} {getVersionString(beatmapInfo)}".Trim(); public static string GetDisplayTitle(this IBeatmapInfo beatmapInfo) => $"{beatmapInfo.Metadata.GetDisplayTitle()} {getVersionString(beatmapInfo)}".Trim();
/// <summary> /// <summary>
/// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields. /// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields.

View File

@ -27,8 +27,12 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
public static string GetDisplayTitle(this IBeatmapMetadataInfo metadataInfo) public static string GetDisplayTitle(this IBeatmapMetadataInfo metadataInfo)
{ {
string author = string.IsNullOrEmpty(metadataInfo.Author.Username) ? string.Empty : $"({metadataInfo.Author})"; string author = string.IsNullOrEmpty(metadataInfo.Author.Username) ? string.Empty : $" ({metadataInfo.Author.Username})";
return $"{metadataInfo.Artist} - {metadataInfo.Title} {author}".Trim();
string artist = string.IsNullOrEmpty(metadataInfo.Artist) ? "unknown artist" : metadataInfo.Artist;
string title = string.IsNullOrEmpty(metadataInfo.Title) ? "unknown title" : metadataInfo.Title;
return $"{artist} - {title}{author}".Trim();
} }
/// <summary> /// <summary>

View File

@ -215,7 +215,7 @@ namespace osu.Game.Beatmaps
var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo(); var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo();
// metadata may have changed; update the path with the standard format. // metadata may have changed; update the path with the standard format.
beatmapInfo.Path = GetValidFilename($"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}].osu"); beatmapInfo.Path = GetValidFilename($"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu");
beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
@ -407,7 +407,7 @@ namespace osu.Game.Beatmaps
beatmap.BeatmapInfo.Ruleset = ruleset; beatmap.BeatmapInfo.Ruleset = ruleset;
// TODO: this should be done in a better place once we actually need to dynamically update it. // TODO: this should be done in a better place once we actually need to dynamically update it.
beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; beatmap.BeatmapInfo.StarRating = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0;
beatmap.BeatmapInfo.Length = calculateLength(beatmap); beatmap.BeatmapInfo.Length = calculateLength(beatmap);
beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength();

View File

@ -38,7 +38,7 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// The maximum star difficulty of all beatmaps in this set. /// The maximum star difficulty of all beatmaps in this set.
/// </summary> /// </summary>
public double MaxStarDifficulty => Beatmaps?.Max(b => b.StarDifficulty) ?? 0; public double MaxStarDifficulty => Beatmaps?.Max(b => b.StarRating) ?? 0;
/// <summary> /// <summary>
/// The maximum playable length in milliseconds of all beatmaps in this set. /// The maximum playable length in milliseconds of all beatmaps in this set.

View File

@ -64,7 +64,7 @@ namespace osu.Game.Beatmaps
BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b => BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b =>
{ {
double difference = b.StarDifficulty - recommendation; double difference = b.StarRating - recommendation;
return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder
}).FirstOrDefault(); }).FirstOrDefault();

View File

@ -251,7 +251,7 @@ namespace osu.Game.Beatmaps.Formats
break; break;
case @"Version": case @"Version":
beatmap.BeatmapInfo.Version = pair.Value; beatmap.BeatmapInfo.DifficultyName = pair.Value;
break; break;
case @"Source": case @"Source":

View File

@ -130,7 +130,7 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine(FormattableString.Invariant($"Artist: {beatmap.Metadata.Artist}")); writer.WriteLine(FormattableString.Invariant($"Artist: {beatmap.Metadata.Artist}"));
if (!string.IsNullOrEmpty(beatmap.Metadata.ArtistUnicode)) writer.WriteLine(FormattableString.Invariant($"ArtistUnicode: {beatmap.Metadata.ArtistUnicode}")); if (!string.IsNullOrEmpty(beatmap.Metadata.ArtistUnicode)) writer.WriteLine(FormattableString.Invariant($"ArtistUnicode: {beatmap.Metadata.ArtistUnicode}"));
writer.WriteLine(FormattableString.Invariant($"Creator: {beatmap.Metadata.Author.Username}")); writer.WriteLine(FormattableString.Invariant($"Creator: {beatmap.Metadata.Author.Username}"));
writer.WriteLine(FormattableString.Invariant($"Version: {beatmap.BeatmapInfo.Version}")); writer.WriteLine(FormattableString.Invariant($"Version: {beatmap.BeatmapInfo.DifficultyName}"));
if (!string.IsNullOrEmpty(beatmap.Metadata.Source)) writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}")); if (!string.IsNullOrEmpty(beatmap.Metadata.Source)) writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}"));
if (!string.IsNullOrEmpty(beatmap.Metadata.Tags)) writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}")); if (!string.IsNullOrEmpty(beatmap.Metadata.Tags)) writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}"));
if (beatmap.BeatmapInfo.OnlineBeatmapID != null) writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineBeatmapID}")); if (beatmap.BeatmapInfo.OnlineBeatmapID != null) writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineBeatmapID}"));

View File

@ -15,6 +15,7 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Extensions;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
using osu.Game.IPC; using osu.Game.IPC;
@ -192,7 +193,7 @@ namespace osu.Game.Database
else else
{ {
notification.CompletionText = imported.Count == 1 notification.CompletionText = imported.Count == 1
? $"Imported {imported.First().Value}!" ? $"Imported {imported.First().Value.GetDisplayString()}!"
: $"Imported {imported.Count} {HumanisedModelName}s!"; : $"Imported {imported.Count} {HumanisedModelName}s!";
if (imported.Count > 0 && PostImport != null) if (imported.Count > 0 && PostImport != null)

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Humanizer; using Humanizer;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Extensions;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
@ -50,7 +51,7 @@ namespace osu.Game.Database
DownloadNotification notification = new DownloadNotification DownloadNotification notification = new DownloadNotification
{ {
Text = $"Downloading {request.Model}", Text = $"Downloading {request.Model.GetDisplayString()}",
}; };
request.DownloadProgressed += progress => request.DownloadProgressed += progress =>

View File

@ -0,0 +1,61 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Extensions
{
public static class ModelExtensions
{
/// <summary>
/// Returns a user-facing string representing the <paramref name="model"/>.
/// </summary>
/// <remarks>
/// <para>
/// Non-interface types without special handling will fall back to <see cref="object.ToString()"/>.
/// </para>
/// <para>
/// Warning: This method is _purposefully_ not called <c>GetDisplayTitle()</c> like the others, because otherwise
/// extension method type inference rules cause this method to call itself and cause a stack overflow.
/// </para>
/// </remarks>
public static string GetDisplayString(this object model)
{
string result = null;
switch (model)
{
case IBeatmapSetInfo beatmapSetInfo:
result = beatmapSetInfo.Metadata?.GetDisplayTitle();
break;
case IBeatmapInfo beatmapInfo:
result = beatmapInfo.GetDisplayTitle();
break;
case IBeatmapMetadataInfo metadataInfo:
result = metadataInfo.GetDisplayTitle();
break;
case IScoreInfo scoreInfo:
result = scoreInfo.GetDisplayTitle();
break;
case IRulesetInfo rulesetInfo:
result = rulesetInfo.Name;
break;
case IUser user:
result = user.Username;
break;
}
// fallback in case none of the above happens to match.
result ??= model.ToString();
return result;
}
}
}

View File

@ -4,6 +4,7 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;
@ -161,9 +162,10 @@ namespace osu.Game.Online
builder.AddNewtonsoftJsonProtocol(options => builder.AddNewtonsoftJsonProtocol(options =>
{ {
options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
// TODO: This should only be required to be `TypeNameHandling.Auto`. options.PayloadSerializerSettings.Converters = new List<JsonConverter>
// See usage in osu-server-spectator for further documentation as to why this is required. {
options.PayloadSerializerSettings.TypeNameHandling = TypeNameHandling.All; new SignalRDerivedTypeWorkaroundJsonConverter(),
};
}); });
} }

View File

@ -16,7 +16,6 @@ namespace osu.Game.Online.Multiplayer
[Serializable] [Serializable]
[MessagePackObject] [MessagePackObject]
[Union(0, typeof(TeamVersusRoomState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. [Union(0, typeof(TeamVersusRoomState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
// TODO: abstract breaks json serialisation. attention will be required for iOS support (unless we get messagepack AOT working instead).
public abstract class MatchRoomState public abstract class MatchRoomState
{ {
} }

View File

@ -16,7 +16,6 @@ namespace osu.Game.Online.Multiplayer
[Serializable] [Serializable]
[MessagePackObject] [MessagePackObject]
[Union(0, typeof(TeamVersusUserState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types. [Union(0, typeof(TeamVersusUserState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
// TODO: abstract breaks json serialisation. attention will be required for iOS support (unless we get messagepack AOT working instead).
public abstract class MatchUserState public abstract class MatchUserState
{ {
} }

View File

@ -0,0 +1,60 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace osu.Game.Online
{
/// <summary>
/// A type of <see cref="JsonConverter"/> that serializes a subset of types used in multiplayer/spectator communication that
/// derive from a known base type. This is a safe alternative to using <see cref="TypeNameHandling.Auto"/> or <see cref="TypeNameHandling.All"/>,
/// which are known to have security issues.
/// </summary>
public class SignalRDerivedTypeWorkaroundJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) =>
SignalRUnionWorkaroundResolver.BASE_TYPES.Contains(objectType) ||
SignalRUnionWorkaroundResolver.DERIVED_TYPES.Contains(objectType);
public override object? ReadJson(JsonReader reader, Type objectType, object? o, JsonSerializer jsonSerializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
JObject obj = JObject.Load(reader);
string type = (string)obj[@"$dtype"]!;
var resolvedType = SignalRUnionWorkaroundResolver.DERIVED_TYPES.Single(t => t.Name == type);
object? instance = Activator.CreateInstance(resolvedType);
jsonSerializer.Populate(obj["$value"]!.CreateReader(), instance);
return instance;
}
public override void WriteJson(JsonWriter writer, object? o, JsonSerializer serializer)
{
if (o == null)
{
writer.WriteNull();
return;
}
writer.WriteStartObject();
writer.WritePropertyName(@"$dtype");
serializer.Serialize(writer, o.GetType().Name);
writer.WritePropertyName(@"$value");
writer.WriteRawValue(JsonConvert.SerializeObject(o));
writer.WriteEndObject();
}
}
}

View File

@ -20,7 +20,22 @@ namespace osu.Game.Online
public static readonly MessagePackSerializerOptions OPTIONS = public static readonly MessagePackSerializerOptions OPTIONS =
MessagePackSerializerOptions.Standard.WithResolver(new SignalRUnionWorkaroundResolver()); MessagePackSerializerOptions.Standard.WithResolver(new SignalRUnionWorkaroundResolver());
private static readonly Dictionary<Type, IMessagePackFormatter> formatter_map = new Dictionary<Type, IMessagePackFormatter> public static readonly IReadOnlyList<Type> BASE_TYPES = new[]
{
typeof(MatchServerEvent),
typeof(MatchUserRequest),
typeof(MatchRoomState),
typeof(MatchUserState),
};
public static readonly IReadOnlyList<Type> DERIVED_TYPES = new[]
{
typeof(ChangeTeamRequest),
typeof(TeamVersusRoomState),
typeof(TeamVersusUserState),
};
private static readonly IReadOnlyDictionary<Type, IMessagePackFormatter> formatter_map = new Dictionary<Type, IMessagePackFormatter>
{ {
{ typeof(TeamVersusUserState), new TypeRedirectingFormatter<TeamVersusUserState, MatchUserState>() }, { typeof(TeamVersusUserState), new TypeRedirectingFormatter<TeamVersusUserState, MatchUserState>() },
{ typeof(TeamVersusRoomState), new TypeRedirectingFormatter<TeamVersusRoomState, MatchRoomState>() }, { typeof(TeamVersusRoomState), new TypeRedirectingFormatter<TeamVersusRoomState, MatchRoomState>() },

View File

@ -23,9 +23,16 @@ namespace osu.Game.Overlays.Notifications
{ {
private const float loading_spinner_size = 22; private const float loading_spinner_size = 22;
private LocalisableString text;
public LocalisableString Text public LocalisableString Text
{ {
set => Schedule(() => textDrawable.Text = value); get => text;
set
{
text = value;
Schedule(() => textDrawable.Text = text);
}
} }
public string CompletionText { get; set; } = "Task has completed!"; public string CompletionText { get; set; } = "Task has completed!";

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks namespace osu.Game.Rulesets.Edit.Checks
@ -48,11 +49,15 @@ namespace osu.Game.Rulesets.Edit.Checks
yield return new IssueTemplateLowResolution(this).Create(texture.Width, texture.Height); yield return new IssueTemplateLowResolution(this).Create(texture.Width, texture.Height);
string storagePath = context.Beatmap.BeatmapInfo.BeatmapSet.GetPathForFile(backgroundFile); string storagePath = context.Beatmap.BeatmapInfo.BeatmapSet.GetPathForFile(backgroundFile);
double filesizeMb = context.WorkingBeatmap.GetStream(storagePath).Length / (1024d * 1024d);
using (Stream stream = context.WorkingBeatmap.GetStream(storagePath))
{
double filesizeMb = stream.Length / (1024d * 1024d);
if (filesizeMb > max_filesize_mb) if (filesizeMb > max_filesize_mb)
yield return new IssueTemplateTooUncompressed(this).Create(filesizeMb); yield return new IssueTemplateTooUncompressed(this).Create(filesizeMb);
} }
}
public class IssueTemplateTooHighResolution : IssueTemplate public class IssueTemplateTooHighResolution : IssueTemplate
{ {

View File

@ -0,0 +1,62 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Mods
{
public abstract class ModNoScope : Mod, IApplicableToScoreProcessor, IApplicableToPlayer
{
public override string Name => "No Scope";
public override string Acronym => "NS";
public override ModType Type => ModType.Fun;
public override IconUsage? Icon => FontAwesome.Solid.EyeSlash;
public override double ScoreMultiplier => 1;
/// <summary>
/// Slightly higher than the cutoff for <see cref="Drawable.IsPresent"/>.
/// </summary>
protected const float MIN_ALPHA = 0.0002f;
protected const float TRANSITION_DURATION = 100;
protected BindableNumber<int> CurrentCombo;
protected IBindable<bool> IsBreakTime;
protected float ComboBasedAlpha;
public abstract BindableInt HiddenComboCount { get; }
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
public void ApplyToPlayer(Player player)
{
IsBreakTime = player.IsBreakTime.GetBoundCopy();
}
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
{
if (HiddenComboCount.Value == 0) return;
CurrentCombo = scoreProcessor.Combo.GetBoundCopy();
CurrentCombo.BindValueChanged(combo =>
{
ComboBasedAlpha = Math.Max(MIN_ALPHA, 1 - (float)combo.NewValue / HiddenComboCount.Value);
}, true);
}
}
public class HiddenComboSlider : OsuSliderBar<int>
{
public override LocalisableString TooltipText => Current.Value == 0 ? "always hidden" : base.TooltipText;
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
namespace osu.Game.Rulesets
{
public class RulesetLoadException : Exception
{
public RulesetLoadException(string message)
: base(@$"Ruleset could not be loaded ({message})")
{
}
}
}

View File

@ -7,7 +7,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using osu.Framework; using osu.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Database; using osu.Game.Database;
@ -129,12 +128,15 @@ namespace osu.Game.Rulesets
{ {
try try
{ {
var instanceInfo = ((Ruleset)Activator.CreateInstance(Type.GetType(r.InstantiationInfo).AsNonNull())).RulesetInfo; var resolvedType = Type.GetType(r.InstantiationInfo)
?? throw new RulesetLoadException(@"Type could not be resolved");
var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo
?? throw new RulesetLoadException(@"Instantiation failure");
r.Name = instanceInfo.Name; r.Name = instanceInfo.Name;
r.ShortName = instanceInfo.ShortName; r.ShortName = instanceInfo.ShortName;
r.InstantiationInfo = instanceInfo.InstantiationInfo; r.InstantiationInfo = instanceInfo.InstantiationInfo;
r.Available = true; r.Available = true;
} }
catch catch

View File

@ -225,7 +225,7 @@ namespace osu.Game.Scoring
return clone; return clone;
} }
public override string ToString() => $"{User} playing {BeatmapInfo}"; public override string ToString() => this.GetDisplayTitle();
public bool Equals(ScoreInfo other) public bool Equals(ScoreInfo other)
{ {

View File

@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
namespace osu.Game.Scoring
{
public static class ScoreInfoExtensions
{
/// <summary>
/// A user-presentable display title representing this score.
/// </summary>
public static string GetDisplayTitle(this IScoreInfo scoreInfo) => $"{scoreInfo.User.Username} playing {scoreInfo.Beatmap.GetDisplayTitle()}";
}
}

View File

@ -13,7 +13,7 @@ namespace osu.Game.Screens.Edit.Components.Menus
public BeatmapInfo BeatmapInfo { get; } public BeatmapInfo BeatmapInfo { get; }
public DifficultyMenuItem(BeatmapInfo beatmapInfo, bool selected, Action<BeatmapInfo> difficultyChangeFunc) public DifficultyMenuItem(BeatmapInfo beatmapInfo, bool selected, Action<BeatmapInfo> difficultyChangeFunc)
: base(beatmapInfo.Version ?? "(unnamed)", null) : base(beatmapInfo.DifficultyName ?? "(unnamed)", null)
{ {
BeatmapInfo = beatmapInfo; BeatmapInfo = beatmapInfo;
State.Value = selected; State.Value = selected;

View File

@ -7,29 +7,26 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Compose.Components.Timeline;
namespace osu.Game.Screens.Edit.Compose namespace osu.Game.Screens.Edit.Compose
{ {
public class ComposeScreen : EditorScreenWithTimeline, IKeyBindingHandler<PlatformAction> public class ComposeScreen : EditorScreenWithTimeline
{ {
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; }
[Resolved] [Resolved]
private GameHost host { get; set; } private GameHost host { get; set; }
[Resolved] [Resolved]
private EditorClock clock { get; set; } private EditorClock clock { get; set; }
private Bindable<string> clipboard { get; set; }
private HitObjectComposer composer; private HitObjectComposer composer;
public ComposeScreen() public ComposeScreen()
@ -76,18 +73,65 @@ namespace osu.Game.Screens.Edit.Compose
return new EditorSkinProvidingContainer(EditorBeatmap).WithChild(content); return new EditorSkinProvidingContainer(EditorBeatmap).WithChild(content);
} }
#region Input Handling [BackgroundDependencyLoader]
private void load(EditorClipboard clipboard)
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
{ {
if (e.Action == PlatformAction.Copy) this.clipboard = clipboard.Content.GetBoundCopy();
host.GetClipboard()?.SetText(formatSelectionAsString());
return false;
} }
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e) protected override void LoadComplete()
{ {
base.LoadComplete();
EditorBeatmap.SelectedHitObjects.BindCollectionChanged((_, __) => updateClipboardActionAvailability());
clipboard.BindValueChanged(_ => updateClipboardActionAvailability(), true);
}
#region Clipboard operations
protected override void PerformCut()
{
base.PerformCut();
Copy();
EditorBeatmap.RemoveRange(EditorBeatmap.SelectedHitObjects.ToArray());
}
protected override void PerformCopy()
{
base.PerformCopy();
clipboard.Value = new ClipboardContent(EditorBeatmap).Serialize();
host.GetClipboard()?.SetText(formatSelectionAsString());
}
protected override void PerformPaste()
{
base.PerformPaste();
var objects = clipboard.Value.Deserialize<ClipboardContent>().HitObjects;
Debug.Assert(objects.Any());
double timeOffset = clock.CurrentTime - objects.Min(o => o.StartTime);
foreach (var h in objects)
h.StartTime += timeOffset;
EditorBeatmap.BeginChange();
EditorBeatmap.SelectedHitObjects.Clear();
EditorBeatmap.AddRange(objects);
EditorBeatmap.SelectedHitObjects.AddRange(objects);
EditorBeatmap.EndChange();
}
private void updateClipboardActionAvailability()
{
CanCut.Value = CanCopy.Value = EditorBeatmap.SelectedHitObjects.Any();
CanPaste.Value = !string.IsNullOrEmpty(clipboard.Value);
} }
private string formatSelectionAsString() private string formatSelectionAsString()

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework; using osu.Framework;
@ -24,7 +23,6 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.IO.Serialization;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -105,6 +103,9 @@ namespace osu.Game.Screens.Edit
[Resolved] [Resolved]
private MusicController music { get; set; } private MusicController music { get; set; }
[Cached]
public readonly EditorClipboard Clipboard = new EditorClipboard();
public Editor(EditorLoader loader = null) public Editor(EditorLoader loader = null)
{ {
this.loader = loader; this.loader = loader;
@ -180,10 +181,6 @@ namespace osu.Game.Screens.Edit
OsuMenuItem undoMenuItem; OsuMenuItem undoMenuItem;
OsuMenuItem redoMenuItem; OsuMenuItem redoMenuItem;
EditorMenuItem cutMenuItem;
EditorMenuItem copyMenuItem;
EditorMenuItem pasteMenuItem;
AddInternal(new OsuContextMenuContainer AddInternal(new OsuContextMenuContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -299,19 +296,15 @@ namespace osu.Game.Screens.Edit
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
editorBeatmap.SelectedHitObjects.BindCollectionChanged((_, __) =>
{
bool hasObjects = editorBeatmap.SelectedHitObjects.Count > 0;
cutMenuItem.Action.Disabled = !hasObjects;
copyMenuItem.Action.Disabled = !hasObjects;
}, true);
clipboard.BindValueChanged(content => pasteMenuItem.Action.Disabled = string.IsNullOrEmpty(content.NewValue));
menuBar.Mode.ValueChanged += onModeChanged; menuBar.Mode.ValueChanged += onModeChanged;
} }
protected override void LoadComplete()
{
base.LoadComplete();
setUpClipboardActionAvailability();
}
/// <summary> /// <summary>
/// If the beatmap's track has changed, this method must be called to keep the editor in a valid state. /// If the beatmap's track has changed, this method must be called to keep the editor in a valid state.
/// </summary> /// </summary>
@ -324,7 +317,7 @@ namespace osu.Game.Screens.Edit
public void RestoreState([NotNull] EditorState state) => Schedule(() => public void RestoreState([NotNull] EditorState state) => Schedule(() =>
{ {
clock.Seek(state.Time); clock.Seek(state.Time);
clipboard.Value = state.ClipboardContent; Clipboard.Content.Value = state.ClipboardContent;
}); });
protected void Save() protected void Save()
@ -561,45 +554,37 @@ namespace osu.Game.Screens.Edit
this.Exit(); this.Exit();
} }
private readonly Bindable<string> clipboard = new Bindable<string>(); #region Clipboard support
protected void Cut() private EditorMenuItem cutMenuItem;
private EditorMenuItem copyMenuItem;
private EditorMenuItem pasteMenuItem;
private readonly BindableWithCurrent<bool> canCut = new BindableWithCurrent<bool>();
private readonly BindableWithCurrent<bool> canCopy = new BindableWithCurrent<bool>();
private readonly BindableWithCurrent<bool> canPaste = new BindableWithCurrent<bool>();
private void setUpClipboardActionAvailability()
{ {
Copy(); canCut.Current.BindValueChanged(cut => cutMenuItem.Action.Disabled = !cut.NewValue, true);
editorBeatmap.RemoveRange(editorBeatmap.SelectedHitObjects.ToArray()); canCopy.Current.BindValueChanged(copy => copyMenuItem.Action.Disabled = !copy.NewValue, true);
canPaste.Current.BindValueChanged(paste => pasteMenuItem.Action.Disabled = !paste.NewValue, true);
} }
protected void Copy() private void rebindClipboardBindables()
{ {
if (editorBeatmap.SelectedHitObjects.Count == 0) canCut.Current = currentScreen.CanCut;
return; canCopy.Current = currentScreen.CanCopy;
canPaste.Current = currentScreen.CanPaste;
clipboard.Value = new ClipboardContent(editorBeatmap).Serialize();
} }
protected void Paste() protected void Cut() => currentScreen?.Cut();
{
if (string.IsNullOrEmpty(clipboard.Value))
return;
var objects = clipboard.Value.Deserialize<ClipboardContent>().HitObjects; protected void Copy() => currentScreen?.Copy();
Debug.Assert(objects.Any()); protected void Paste() => currentScreen?.Paste();
double timeOffset = clock.CurrentTime - objects.Min(o => o.StartTime); #endregion
foreach (var h in objects)
h.StartTime += timeOffset;
editorBeatmap.BeginChange();
editorBeatmap.SelectedHitObjects.Clear();
editorBeatmap.AddRange(objects);
editorBeatmap.SelectedHitObjects.AddRange(objects);
editorBeatmap.EndChange();
}
protected void Undo() => changeHandler.RestoreState(-1); protected void Undo() => changeHandler.RestoreState(-1);
@ -677,6 +662,7 @@ namespace osu.Game.Screens.Edit
finally finally
{ {
updateSampleDisabledState(); updateSampleDisabledState();
rebindClipboardBindables();
} }
} }
@ -737,7 +723,7 @@ namespace osu.Game.Screens.Edit
if (difficultyItems.Count > 0) if (difficultyItems.Count > 0)
difficultyItems.Add(new EditorMenuItemSpacer()); difficultyItems.Add(new EditorMenuItemSpacer());
foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarDifficulty)) foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarRating))
difficultyItems.Add(createDifficultyMenuItem(beatmap)); difficultyItems.Add(createDifficultyMenuItem(beatmap));
} }
@ -757,7 +743,7 @@ namespace osu.Game.Screens.Edit
protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, new EditorState protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, new EditorState
{ {
Time = clock.CurrentTimeAccurate, Time = clock.CurrentTimeAccurate,
ClipboardContent = editorBeatmap.BeatmapInfo.RulesetID == nextBeatmap.RulesetID ? clipboard.Value : string.Empty ClipboardContent = editorBeatmap.BeatmapInfo.RulesetID == nextBeatmap.RulesetID ? Clipboard.Content.Value : string.Empty
}); });
private void cancelExit() private void cancelExit()

View File

@ -0,0 +1,15 @@
// 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;
namespace osu.Game.Screens.Edit
{
/// <summary>
/// Wraps the contents of the editor clipboard.
/// </summary>
public class EditorClipboard
{
public Bindable<string> Content { get; } = new Bindable<string>();
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
@ -49,5 +50,54 @@ namespace osu.Game.Screens.Edit
this.ScaleTo(0.98f, 200, Easing.OutQuint) this.ScaleTo(0.98f, 200, Easing.OutQuint)
.FadeOut(200, Easing.OutQuint); .FadeOut(200, Easing.OutQuint);
} }
#region Clipboard operations
public BindableBool CanCut { get; } = new BindableBool();
/// <summary>
/// Performs a "cut to clipboard" operation appropriate for the given screen.
/// </summary>
protected virtual void PerformCut()
{
}
public void Cut()
{
if (CanCut.Value)
PerformCut();
}
public BindableBool CanCopy { get; } = new BindableBool();
/// <summary>
/// Performs a "copy to clipboard" operation appropriate for the given screen.
/// </summary>
protected virtual void PerformCopy()
{
}
public virtual void Copy()
{
if (CanCopy.Value)
PerformCopy();
}
public BindableBool CanPaste { get; } = new BindableBool();
/// <summary>
/// Performs a "paste from clipboard" operation appropriate for the given screen.
/// </summary>
protected virtual void PerformPaste()
{
}
public virtual void Paste()
{
if (CanPaste.Value)
PerformPaste();
}
#endregion
} }
} }

View File

@ -47,7 +47,7 @@ namespace osu.Game.Screens.Edit.Setup
Empty(), Empty(),
creatorTextBox = createTextBox<LabelledTextBox>("Creator", metadata.Author.Username), creatorTextBox = createTextBox<LabelledTextBox>("Creator", metadata.Author.Username),
difficultyTextBox = createTextBox<LabelledTextBox>("Difficulty Name", Beatmap.BeatmapInfo.Version), difficultyTextBox = createTextBox<LabelledTextBox>("Difficulty Name", Beatmap.BeatmapInfo.DifficultyName),
sourceTextBox = createTextBox<LabelledTextBox>("Source", metadata.Source), sourceTextBox = createTextBox<LabelledTextBox>("Source", metadata.Source),
tagsTextBox = createTextBox<LabelledTextBox>("Tags", metadata.Tags) tagsTextBox = createTextBox<LabelledTextBox>("Tags", metadata.Tags)
}; };
@ -111,7 +111,7 @@ namespace osu.Game.Screens.Edit.Setup
Beatmap.Metadata.Title = RomanisedTitleTextBox.Current.Value; Beatmap.Metadata.Title = RomanisedTitleTextBox.Current.Value;
Beatmap.Metadata.AuthorString = creatorTextBox.Current.Value; Beatmap.Metadata.AuthorString = creatorTextBox.Current.Value;
Beatmap.BeatmapInfo.Version = difficultyTextBox.Current.Value; Beatmap.BeatmapInfo.DifficultyName = difficultyTextBox.Current.Value;
Beatmap.Metadata.Source = sourceTextBox.Current.Value; Beatmap.Metadata.Source = sourceTextBox.Current.Value;
Beatmap.Metadata.Tags = tagsTextBox.Current.Value; Beatmap.Metadata.Tags = tagsTextBox.Current.Value;
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -214,10 +215,16 @@ namespace osu.Game.Screens.Menu
} }
else if (!api.IsLoggedIn) else if (!api.IsLoggedIn)
{ {
logo.Action += displayLogin; // copy out old action to avoid accidentally capturing logo.Action in closure, causing a self-reference loop.
var previousAction = logo.Action;
// we want to hook into logo.Action to display the login overlay, but also preserve the return value of the old action.
// therefore pass the old action to displayLogin, so that it can return that value.
// this ensures that the OsuLogo sample does not play when it is not desired.
logo.Action = () => displayLogin(previousAction);
} }
bool displayLogin() bool displayLogin(Func<bool> originalAction)
{ {
if (!loginDisplayed.Value) if (!loginDisplayed.Value)
{ {
@ -225,7 +232,7 @@ namespace osu.Game.Screens.Menu
loginDisplayed.Value = true; loginDisplayed.Value = true;
} }
return true; return originalAction.Invoke();
} }
} }

View File

@ -29,23 +29,29 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
private OsuClickableContainer clickableContent;
public TeamDisplay(MultiplayerRoomUser user) public TeamDisplay(MultiplayerRoomUser user)
{ {
this.user = user; this.user = user;
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
Width = 15;
AutoSizeAxes = Axes.X;
Margin = new MarginPadding { Horizontal = 3 }; Margin = new MarginPadding { Horizontal = 3 };
Alpha = 0;
Scale = new Vector2(0, 1);
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio) private void load(AudioManager audio)
{ {
box = new Container InternalChild = clickableContent = new OsuClickableContainer
{
Width = 15,
Alpha = 0,
Scale = new Vector2(0, 1),
RelativeSizeAxes = Axes.Y,
Child = box = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
CornerRadius = 5, CornerRadius = 5,
@ -60,21 +66,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
} }
}
}; };
if (Client.LocalUser?.Equals(user) == true) if (Client.LocalUser?.Equals(user) == true)
{ {
InternalChild = new OsuClickableContainer clickableContent.Action = changeTeam;
{ clickableContent.TooltipText = "Change team";
RelativeSizeAxes = Axes.Both,
TooltipText = "Change team",
Action = changeTeam,
Child = box
};
}
else
{
InternalChild = box;
} }
sampleTeamSwap = audio.Samples.Get(@"Multiplayer/team-swap"); sampleTeamSwap = audio.Samples.Get(@"Multiplayer/team-swap");
@ -88,7 +86,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
}); });
} }
private int? displayedTeam; public int? DisplayedTeam { get; private set; }
protected override void OnRoomUpdated() protected override void OnRoomUpdated()
{ {
@ -102,28 +100,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
int? newTeam = (userRoomState as TeamVersusUserState)?.TeamID; int? newTeam = (userRoomState as TeamVersusUserState)?.TeamID;
if (newTeam == displayedTeam) if (newTeam == DisplayedTeam)
return; return;
// only play the sample if an already valid team changes to another valid team. // only play the sample if an already valid team changes to another valid team.
// this avoids playing a sound for each user if the match type is changed to/from a team mode. // this avoids playing a sound for each user if the match type is changed to/from a team mode.
if (newTeam != null && displayedTeam != null) if (newTeam != null && DisplayedTeam != null)
sampleTeamSwap?.Play(); sampleTeamSwap?.Play();
displayedTeam = newTeam; DisplayedTeam = newTeam;
if (displayedTeam != null) if (DisplayedTeam != null)
{ {
box.FadeColour(getColourForTeam(displayedTeam.Value), duration, Easing.OutQuint); box.FadeColour(getColourForTeam(DisplayedTeam.Value), duration, Easing.OutQuint);
box.ScaleTo(new Vector2(box.Scale.X < 0 ? 1 : -1, 1), duration, Easing.OutQuint); box.ScaleTo(new Vector2(box.Scale.X < 0 ? 1 : -1, 1), duration, Easing.OutQuint);
this.ScaleTo(Vector2.One, duration, Easing.OutQuint); clickableContent.ScaleTo(Vector2.One, duration, Easing.OutQuint);
this.FadeIn(duration); clickableContent.FadeIn(duration);
} }
else else
{ {
this.ScaleTo(new Vector2(0, 1), duration, Easing.OutQuint); clickableContent.ScaleTo(new Vector2(0, 1), duration, Easing.OutQuint);
this.FadeOut(duration); clickableContent.FadeOut(duration);
} }
} }

View File

@ -126,7 +126,7 @@ namespace osu.Game.Screens.Play
{ {
new OsuSpriteText new OsuSpriteText
{ {
Text = beatmap?.BeatmapInfo?.Version, Text = beatmap?.BeatmapInfo?.DifficultyName,
Font = OsuFont.GetFont(size: 26, italics: true), Font = OsuFont.GetFont(size: 26, italics: true),
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,

View File

@ -164,7 +164,7 @@ namespace osu.Game.Screens.Ranking.Expanded
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Text = beatmap.Version, Text = beatmap.DifficultyName,
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold),
}, },
new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12)) new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12))

View File

@ -229,7 +229,7 @@ namespace osu.Game.Screens.Select
{ {
VersionLabel = new OsuSpriteText VersionLabel = new OsuSpriteText
{ {
Text = beatmapInfo.Version, Text = beatmapInfo.DifficultyName,
Font = OsuFont.GetFont(size: 24, italics: true), Font = OsuFont.GetFont(size: 24, italics: true),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Truncate = true, Truncate = true,
@ -324,7 +324,7 @@ namespace osu.Game.Screens.Select
}); });
// no difficulty means it can't have a status to show // no difficulty means it can't have a status to show
if (beatmapInfo.Version == null) if (beatmapInfo.DifficultyName == null)
StatusPill.Hide(); StatusPill.Hide();
addInfoLabels(); addInfoLabels();

View File

@ -38,7 +38,7 @@ namespace osu.Game.Screens.Select.Carousel
return; return;
} }
match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarDifficulty); match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarRating);
match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(BeatmapInfo.BaseDifficulty.ApproachRate); match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(BeatmapInfo.BaseDifficulty.ApproachRate);
match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(BeatmapInfo.BaseDifficulty.DrainRate); match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(BeatmapInfo.BaseDifficulty.DrainRate);
match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(BeatmapInfo.BaseDifficulty.CircleSize); match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(BeatmapInfo.BaseDifficulty.CircleSize);
@ -53,7 +53,7 @@ namespace osu.Game.Screens.Select.Carousel
match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) || match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) ||
criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode); criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode);
match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarDifficulty); match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating);
if (match) if (match)
{ {
@ -92,7 +92,7 @@ namespace osu.Game.Screens.Select.Carousel
int ruleset = BeatmapInfo.RulesetID.CompareTo(otherBeatmap.BeatmapInfo.RulesetID); int ruleset = BeatmapInfo.RulesetID.CompareTo(otherBeatmap.BeatmapInfo.RulesetID);
if (ruleset != 0) return ruleset; if (ruleset != 0) return ruleset;
return BeatmapInfo.StarDifficulty.CompareTo(otherBeatmap.BeatmapInfo.StarDifficulty); return BeatmapInfo.StarRating.CompareTo(otherBeatmap.BeatmapInfo.StarRating);
} }
} }

View File

@ -84,7 +84,7 @@ namespace osu.Game.Screens.Select.Carousel
return compareUsingAggregateMax(otherSet, b => b.Length); return compareUsingAggregateMax(otherSet, b => b.Length);
case SortMode.Difficulty: case SortMode.Difficulty:
return compareUsingAggregateMax(otherSet, b => b.StarDifficulty); return compareUsingAggregateMax(otherSet, b => b.StarRating);
} }
} }

View File

@ -129,7 +129,7 @@ namespace osu.Game.Screens.Select.Carousel
{ {
new OsuSpriteText new OsuSpriteText
{ {
Text = beatmapInfo.Version, Text = beatmapInfo.DifficultyName,
Font = OsuFont.GetFont(size: 20), Font = OsuFont.GetFont(size: 20),
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft Origin = Anchor.BottomLeft

View File

@ -253,7 +253,7 @@ namespace osu.Game.Stores
var beatmap = new RealmBeatmap(ruleset, difficulty, metadata) var beatmap = new RealmBeatmap(ruleset, difficulty, metadata)
{ {
Hash = hash, Hash = hash,
DifficultyName = decodedInfo.Version, DifficultyName = decodedInfo.DifficultyName,
OnlineID = decodedInfo.OnlineBeatmapID ?? -1, OnlineID = decodedInfo.OnlineBeatmapID ?? -1,
AudioLeadIn = decodedInfo.AudioLeadIn, AudioLeadIn = decodedInfo.AudioLeadIn,
StackLeniency = decodedInfo.StackLeniency, StackLeniency = decodedInfo.StackLeniency,

View File

@ -147,19 +147,15 @@ namespace osu.Game.Stores
{ {
try try
{ {
var type = Type.GetType(r.InstantiationInfo); var resolvedType = Type.GetType(r.InstantiationInfo)
?? throw new RulesetLoadException(@"Type could not be resolved");
if (type == null) var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo
throw new InvalidOperationException(@"Type resolution failure."); ?? throw new RulesetLoadException(@"Instantiation failure");
var rInstance = (Activator.CreateInstance(type) as Ruleset)?.RulesetInfo; r.Name = instanceInfo.Name;
r.ShortName = instanceInfo.ShortName;
if (rInstance == null) r.InstantiationInfo = instanceInfo.InstantiationInfo;
throw new InvalidOperationException(@"Instantiation failure.");
r.Name = rInstance.Name;
r.ShortName = rInstance.ShortName;
r.InstantiationInfo = rInstance.InstantiationInfo;
r.Available = true; r.Available = true;
detachedRulesets.Add(r.Clone()); detachedRulesets.Add(r.Clone());

View File

@ -72,6 +72,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
// We want the user to be immediately available for testing, so force a scheduler update to run the update-bound continuation. // We want the user to be immediately available for testing, so force a scheduler update to run the update-bound continuation.
Scheduler.Update(); Scheduler.Update();
switch (Room?.MatchState)
{
case TeamVersusRoomState teamVersus:
Debug.Assert(Room != null);
// simulate the server's automatic assignment of users to teams on join.
// the "best" team is the one with the least users on it.
int bestTeam = teamVersus.Teams
.Select(team => (teamID: team.ID, userCount: Room.Users.Count(u => (u.MatchState as TeamVersusUserState)?.TeamID == team.ID)))
.OrderBy(pair => pair.userCount)
.First().teamID;
((IMultiplayerClient)this).MatchUserStateChanged(user.UserID, new TeamVersusUserState { TeamID = bestTeam }).Wait();
break;
}
} }
public void RemoveUser(APIUser user) public void RemoveUser(APIUser user)

View File

@ -228,8 +228,8 @@ namespace osu.Game.Tests.Visual
Checksum = beatmap.MD5Hash, Checksum = beatmap.MD5Hash,
AuthorID = beatmap.Metadata.Author.OnlineID, AuthorID = beatmap.Metadata.Author.OnlineID,
RulesetID = beatmap.RulesetID, RulesetID = beatmap.RulesetID,
StarRating = beatmap.StarDifficulty, StarRating = beatmap.StarRating,
DifficultyName = beatmap.Version, DifficultyName = beatmap.DifficultyName,
} }
} }
}; };