1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-13 12:53:11 +08:00

Merge branch 'master' into editor-clipboard-refactor

This commit is contained in:
Dean Herbert 2021-11-11 01:09:39 +09:00 committed by GitHub
commit 8371307709
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 562 additions and 114 deletions

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:

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

@ -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

@ -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

@ -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

@ -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

@ -127,9 +127,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

@ -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

@ -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

@ -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

@ -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

@ -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,30 @@ 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,
Action = changeTeam,
Child = box = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
CornerRadius = 5, CornerRadius = 5,
@ -60,21 +67,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 +87,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 +101,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);
} }
} }