1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-22 20:23:52 +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:
name: Build only (Android)
runs-on: windows-latest
runs-on: macos-latest
timeout-minutes: 60
steps:
- name: Checkout
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
uses: actions/setup-dotnet@v1
with:
dotnet-version: "5.0.x"
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v1
# Contrary to seemingly any other msbuild, msbuild running on macOS/Mono
# cannot accept .sln(f) files as arguments.
# Build just the main game for now.
- 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:
# 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 CatchModFloatingFruits(),
new CatchModMuted(),
new CatchModNoScope(),
};
default:
@ -188,5 +189,7 @@ namespace osu.Game.Rulesets.Catch
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
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]
private SkinManager skins { get; set; }
[Cached]
private EditorClipboard clipboard = new EditorClipboard();
[SetUpSteps]
public void SetUpSteps()
{

View File

@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
AddAssert("cursor must start visible", () => cursorAlphaAlmostEquals(1));
AddUntilStep("wait for combo", () => Player.ScoreProcessor.Combo.Value >= 2);
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));
}

View File

@ -4,52 +4,29 @@
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Utils;
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 double ScoreMultiplier => 1;
private BindableNumber<int> currentCombo;
private IBindable<bool> isBreakTime;
private PeriodTracker spinnerPeriods;
private float comboBasedAlpha;
[SettingSource(
"Hidden at combo",
"The combo count at which the cursor becomes completely hidden",
SettingControlType = typeof(SettingsSlider<int, HiddenComboSlider>)
)]
public BindableInt HiddenComboCount { get; } = new BindableInt
public override BindableInt HiddenComboCount { get; } = new BindableInt
{
Default = 10,
Value = 10,
@ -57,39 +34,16 @@ namespace osu.Game.Rulesets.Osu.Mods
MaxValue = 50,
};
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
public void ApplyToPlayer(Player player)
{
isBreakTime = player.IsBreakTime.GetBoundCopy();
}
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;
currentCombo = scoreProcessor.Combo.GetBoundCopy();
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));
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.Title = "title";
});
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.Version = "difficulty");
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty");
checkMutations();
@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor
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 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.ArtistUnicode);
Assert.AreEqual("Gamu", metadata.Author.Username);
Assert.AreEqual("Insane", beatmapInfo.Version);
Assert.AreEqual("Insane", beatmapInfo.DifficultyName);
Assert.AreEqual(string.Empty, metadata.Source);
Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", metadata.Tags);
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
BeatmapSetInfo setToUpdate = manager.GetAllUsableBeatmapSets()[0];
Beatmap beatmapToUpdate = (Beatmap)manager.GetWorkingBeatmap(setToUpdate.Beatmaps.First(b => b.RulesetID == 0)).Beatmap;
beatmapToUpdate.BeatmapInfo.Version = "updated";
beatmapToUpdate.BeatmapInfo.DifficultyName = "updated";
manager.Update(setToUpdate);
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
{
@ -863,7 +863,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var beatmap = working.Beatmap;
beatmap.BeatmapInfo.Version = "difficulty";
beatmap.BeatmapInfo.DifficultyName = "difficulty";
beatmap.BeatmapInfo.Metadata = new BeatmapMetadata
{
Artist = "Artist/With\\Slashes",

View File

@ -52,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps
Title = "title",
Author = new APIUser { Username = "creator" }
},
Version = "difficulty"
DifficultyName = "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.
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);
}
@ -103,7 +103,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test]
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();
@ -111,19 +111,32 @@ namespace osu.Game.Tests.Editing.Checks
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>
/// 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>
/// <param name="background">The texture of the background.</param>
/// <param name="fileBytes">The bytes that represent the background file.</param>
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(Texture background, [CanBeNull] byte[] fileBytes = null)
/// <param name="stream">The stream representing the background file.</param>
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>();
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
{
Ruleset = new RulesetInfo { ID = 5 },
StarDifficulty = 4.0d,
StarRating = 4.0d,
BaseDifficulty = new BeatmapDifficulty
{
ApproachRate = 5.0f,
@ -34,7 +34,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
Source = "unit tests",
Tags = "look for tags too",
},
Version = "version as well",
DifficultyName = "version as well",
Length = 2500,
BPM = 160,
BeatDivisor = 12,

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Notifications;
using osu.Game.Tests.Visual;
@ -16,7 +17,30 @@ namespace osu.Game.Tests.Online
private BeatmapManager beatmaps;
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]
private void load(BeatmapManager beatmaps)
@ -26,25 +50,41 @@ namespace osu.Game.Tests.Online
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]
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);
}
[Test]
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());
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);
}
}

View File

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

View File

@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Editing
editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
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()));
@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
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 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,
Username = @"WubWoofWolf",
}
}.CreateScoreInfo(rulesets);
}.CreateScoreInfo(rulesets, CreateBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo);
}
private class TestReplayDownloadButton : ReplayDownloadButton

View File

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

View File

@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Ruleset = new OsuRuleset().RulesetInfo,
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,
BPM = bpm,
BaseDifficulty = new BeatmapDifficulty

View File

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

View File

@ -10,6 +10,7 @@ using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Rooms;
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);
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.Click(MouseButton.Left);
});
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);
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]
@ -127,9 +136,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
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));
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)

View File

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

View File

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

View File

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

View File

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

View File

@ -919,7 +919,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Ruleset = getRuleset(),
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,
BPM = bpm,
BaseDifficulty = new BeatmapDifficulty

View File

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

View File

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

View File

@ -134,12 +134,12 @@ namespace osu.Game.Beatmaps
public double TimelineZoom { get; set; }
// Metadata
public string Version { get; set; }
private string versionString => string.IsNullOrEmpty(Version) ? string.Empty : $"[{Version}]";
[Column("Version")]
public string DifficultyName { get; set; }
[JsonProperty("difficulty_rating")]
public double StarDifficulty { get; set; }
[Column("StarDifficulty")]
public double StarRating { get; set; }
/// <summary>
/// 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; }
[JsonIgnore]
public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarDifficulty);
public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarRating);
public override string ToString() => this.GetDisplayTitle();
@ -182,9 +182,6 @@ namespace osu.Game.Beatmaps
#region Implementation of IBeatmapInfo
[JsonIgnore]
string IBeatmapInfo.DifficultyName => Version;
[JsonIgnore]
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata ?? BeatmapSet?.Metadata ?? new BeatmapMetadata();
@ -197,9 +194,6 @@ namespace osu.Game.Beatmaps
[JsonIgnore]
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
[JsonIgnore]
double IBeatmapInfo.StarRating => StarDifficulty;
#endregion
}
}

View File

@ -11,7 +11,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// A user-presentable display title representing this beatmap.
/// </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>
/// 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>
public static string GetDisplayTitle(this IBeatmapMetadataInfo metadataInfo)
{
string author = string.IsNullOrEmpty(metadataInfo.Author.Username) ? string.Empty : $"({metadataInfo.Author})";
return $"{metadataInfo.Artist} - {metadataInfo.Title} {author}".Trim();
string author = string.IsNullOrEmpty(metadataInfo.Author.Username) ? string.Empty : $" ({metadataInfo.Author.Username})";
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>

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();
// 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();
@ -407,7 +407,7 @@ namespace osu.Game.Beatmaps
beatmap.BeatmapInfo.Ruleset = ruleset;
// 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.BPM = 60000 / beatmap.GetMostCommonBeatLength();

View File

@ -38,7 +38,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// The maximum star difficulty of all beatmaps in this set.
/// </summary>
public double MaxStarDifficulty => Beatmaps?.Max(b => b.StarDifficulty) ?? 0;
public double MaxStarDifficulty => Beatmaps?.Max(b => b.StarRating) ?? 0;
/// <summary>
/// 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 =>
{
double difference = b.StarDifficulty - recommendation;
double difference = b.StarRating - recommendation;
return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder
}).FirstOrDefault();

View File

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

View File

@ -130,7 +130,7 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine(FormattableString.Invariant($"Artist: {beatmap.Metadata.Artist}"));
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($"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.Tags)) writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}"));
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.Platform;
using osu.Framework.Threading;
using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.IPC;
@ -192,7 +193,7 @@ namespace osu.Game.Database
else
{
notification.CompletionText = imported.Count == 1
? $"Imported {imported.First().Value}!"
? $"Imported {imported.First().Value.GetDisplayString()}!"
: $"Imported {imported.Count} {HumanisedModelName}s!";
if (imported.Count > 0 && PostImport != null)

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Humanizer;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Overlays.Notifications;
@ -50,7 +51,7 @@ namespace osu.Game.Database
DownloadNotification notification = new DownloadNotification
{
Text = $"Downloading {request.Model}",
Text = $"Downloading {request.Model.GetDisplayString()}",
};
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
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
@ -161,9 +162,10 @@ namespace osu.Game.Online
builder.AddNewtonsoftJsonProtocol(options =>
{
options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
// TODO: This should only be required to be `TypeNameHandling.Auto`.
// See usage in osu-server-spectator for further documentation as to why this is required.
options.PayloadSerializerSettings.TypeNameHandling = TypeNameHandling.All;
options.PayloadSerializerSettings.Converters = new List<JsonConverter>
{
new SignalRDerivedTypeWorkaroundJsonConverter(),
};
});
}

View File

@ -16,7 +16,6 @@ namespace osu.Game.Online.Multiplayer
[Serializable]
[MessagePackObject]
[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
{
}

View File

@ -16,7 +16,6 @@ namespace osu.Game.Online.Multiplayer
[Serializable]
[MessagePackObject]
[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
{
}

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 =
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(TeamVersusRoomState), new TypeRedirectingFormatter<TeamVersusRoomState, MatchRoomState>() },

View File

@ -23,9 +23,16 @@ namespace osu.Game.Overlays.Notifications
{
private const float loading_spinner_size = 22;
private 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!";

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using osu.Game.Rulesets.Edit.Checks.Components;
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);
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)
yield return new IssueTemplateTooUncompressed(this).Create(filesizeMb);
}
}
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.Reflection;
using osu.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Database;
@ -129,12 +128,15 @@ namespace osu.Game.Rulesets
{
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.ShortName = instanceInfo.ShortName;
r.InstantiationInfo = instanceInfo.InstantiationInfo;
r.Available = true;
}
catch

View File

@ -225,7 +225,7 @@ namespace osu.Game.Scoring
return clone;
}
public override string ToString() => $"{User} playing {BeatmapInfo}";
public override string ToString() => this.GetDisplayTitle();
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 DifficultyMenuItem(BeatmapInfo beatmapInfo, bool selected, Action<BeatmapInfo> difficultyChangeFunc)
: base(beatmapInfo.Version ?? "(unnamed)", null)
: base(beatmapInfo.DifficultyName ?? "(unnamed)", null)
{
BeatmapInfo = beatmapInfo;
State.Value = selected;

View File

@ -7,29 +7,26 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
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.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
namespace osu.Game.Screens.Edit.Compose
{
public class ComposeScreen : EditorScreenWithTimeline, IKeyBindingHandler<PlatformAction>
public class ComposeScreen : EditorScreenWithTimeline
{
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; }
[Resolved]
private GameHost host { get; set; }
[Resolved]
private EditorClock clock { get; set; }
private Bindable<string> clipboard { get; set; }
private HitObjectComposer composer;
public ComposeScreen()
@ -76,18 +73,65 @@ namespace osu.Game.Screens.Edit.Compose
return new EditorSkinProvidingContainer(EditorBeatmap).WithChild(content);
}
#region Input Handling
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
[BackgroundDependencyLoader]
private void load(EditorClipboard clipboard)
{
if (e.Action == PlatformAction.Copy)
host.GetClipboard()?.SetText(formatSelectionAsString());
return false;
this.clipboard = clipboard.Content.GetBoundCopy();
}
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()

View File

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

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.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@ -49,5 +50,54 @@ namespace osu.Game.Screens.Edit
this.ScaleTo(0.98f, 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(),
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),
tagsTextBox = createTextBox<LabelledTextBox>("Tags", metadata.Tags)
};
@ -111,7 +111,7 @@ namespace osu.Game.Screens.Edit.Setup
Beatmap.Metadata.Title = RomanisedTitleTextBox.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.Tags = tagsTextBox.Current.Value;
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -214,10 +215,16 @@ namespace osu.Game.Screens.Menu
}
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)
{
@ -225,7 +232,7 @@ namespace osu.Game.Screens.Menu
loginDisplayed.Value = true;
}
return true;
return originalAction.Invoke();
}
}

View File

@ -29,23 +29,29 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
[Resolved]
private OsuColour colours { get; set; }
private OsuClickableContainer clickableContent;
public TeamDisplay(MultiplayerRoomUser user)
{
this.user = user;
RelativeSizeAxes = Axes.Y;
Width = 15;
AutoSizeAxes = Axes.X;
Margin = new MarginPadding { Horizontal = 3 };
Alpha = 0;
Scale = new Vector2(0, 1);
}
[BackgroundDependencyLoader]
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,
CornerRadius = 5,
@ -60,21 +66,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
};
if (Client.LocalUser?.Equals(user) == true)
{
InternalChild = new OsuClickableContainer
{
RelativeSizeAxes = Axes.Both,
TooltipText = "Change team",
Action = changeTeam,
Child = box
};
}
else
{
InternalChild = box;
clickableContent.Action = changeTeam;
clickableContent.TooltipText = "Change team";
}
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()
{
@ -102,28 +100,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
int? newTeam = (userRoomState as TeamVersusUserState)?.TeamID;
if (newTeam == displayedTeam)
if (newTeam == DisplayedTeam)
return;
// 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.
if (newTeam != null && displayedTeam != null)
if (newTeam != null && DisplayedTeam != null)
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);
this.ScaleTo(Vector2.One, duration, Easing.OutQuint);
this.FadeIn(duration);
clickableContent.ScaleTo(Vector2.One, duration, Easing.OutQuint);
clickableContent.FadeIn(duration);
}
else
{
this.ScaleTo(new Vector2(0, 1), duration, Easing.OutQuint);
this.FadeOut(duration);
clickableContent.ScaleTo(new Vector2(0, 1), duration, Easing.OutQuint);
clickableContent.FadeOut(duration);
}
}

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@ namespace osu.Game.Screens.Select.Carousel
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.DrainRate.HasFilter || criteria.DrainRate.IsInRange(BeatmapInfo.BaseDifficulty.DrainRate);
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) ||
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)
{
@ -92,7 +92,7 @@ namespace osu.Game.Screens.Select.Carousel
int ruleset = BeatmapInfo.RulesetID.CompareTo(otherBeatmap.BeatmapInfo.RulesetID);
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);
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
{
Text = beatmapInfo.Version,
Text = beatmapInfo.DifficultyName,
Font = OsuFont.GetFont(size: 20),
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft

View File

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

View File

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

View File

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