1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 12:17:26 +08:00

Merge branch 'master' into refactor-scrolling-hoc-1

This commit is contained in:
Dan Balasescu 2021-06-14 20:42:38 +09:00 committed by GitHub
commit 84ec378be1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 722 additions and 147 deletions

View File

@ -33,10 +33,10 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2021.524.0",
"version": "2021.608.0",
"commands": [
"localisation"
]
}
}
}
}

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.604.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.609.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.611.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.614.0" />
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

View File

@ -0,0 +1,240 @@
// 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.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Users;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneMessageNotifier : OsuManualInputManagerTestScene
{
private User friend;
private Channel publicChannel;
private Channel privateMessageChannel;
private TestContainer testContainer;
private int messageIdCounter;
[SetUp]
public void Setup()
{
if (API is DummyAPIAccess daa)
{
daa.HandleRequest = dummyAPIHandleRequest;
}
friend = new User { Id = 0, Username = "Friend" };
publicChannel = new Channel { Id = 1, Name = "osu" };
privateMessageChannel = new Channel(friend) { Id = 2, Name = friend.Username, Type = ChannelType.PM };
Schedule(() =>
{
Child = testContainer = new TestContainer(new[] { publicChannel, privateMessageChannel })
{
RelativeSizeAxes = Axes.Both,
};
testContainer.ChatOverlay.Show();
});
}
private bool dummyAPIHandleRequest(APIRequest request)
{
switch (request)
{
case GetMessagesRequest messagesRequest:
messagesRequest.TriggerSuccess(new List<Message>(0));
return true;
case CreateChannelRequest createChannelRequest:
var apiChatChannel = new APIChatChannel
{
RecentMessages = new List<Message>(0),
ChannelID = (int)createChannelRequest.Channel.Id
};
createChannelRequest.TriggerSuccess(apiChatChannel);
return true;
case ListChannelsRequest listChannelsRequest:
listChannelsRequest.TriggerSuccess(new List<Channel>(1) { publicChannel });
return true;
case GetUpdatesRequest updatesRequest:
updatesRequest.TriggerSuccess(new GetUpdatesResponse
{
Messages = new List<Message>(0),
Presence = new List<Channel>(0)
});
return true;
case JoinChannelRequest joinChannelRequest:
joinChannelRequest.TriggerSuccess();
return true;
default:
return false;
}
}
[Test]
public void TestPublicChannelMention()
{
AddStep("switch to PMs", () => testContainer.ChannelManager.CurrentChannel.Value = privateMessageChannel);
AddStep("receive public message", () => receiveMessage(friend, publicChannel, "Hello everyone"));
AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0);
AddStep("receive message containing mention", () => receiveMessage(friend, publicChannel, $"Hello {API.LocalUser.Value.Username.ToLowerInvariant()}!"));
AddAssert("1 notification fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 1);
AddStep("open notification overlay", () => testContainer.NotificationOverlay.Show());
AddStep("click notification", clickNotification<MessageNotifier.MentionNotification>);
AddAssert("chat overlay is open", () => testContainer.ChatOverlay.State.Value == Visibility.Visible);
AddAssert("public channel is selected", () => testContainer.ChannelManager.CurrentChannel.Value == publicChannel);
}
[Test]
public void TestPrivateMessageNotification()
{
AddStep("switch to public channel", () => testContainer.ChannelManager.CurrentChannel.Value = publicChannel);
AddStep("receive PM", () => receiveMessage(friend, privateMessageChannel, $"Hello {API.LocalUser.Value.Username}"));
AddAssert("1 notification fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 1);
AddStep("open notification overlay", () => testContainer.NotificationOverlay.Show());
AddStep("click notification", clickNotification<MessageNotifier.PrivateMessageNotification>);
AddAssert("chat overlay is open", () => testContainer.ChatOverlay.State.Value == Visibility.Visible);
AddAssert("PM channel is selected", () => testContainer.ChannelManager.CurrentChannel.Value == privateMessageChannel);
}
[Test]
public void TestNoNotificationWhenPMChannelOpen()
{
AddStep("switch to PMs", () => testContainer.ChannelManager.CurrentChannel.Value = privateMessageChannel);
AddStep("receive PM", () => receiveMessage(friend, privateMessageChannel, "you're reading this, right?"));
AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0);
}
[Test]
public void TestNoNotificationWhenMentionedInOpenPublicChannel()
{
AddStep("switch to public channel", () => testContainer.ChannelManager.CurrentChannel.Value = publicChannel);
AddStep("receive mention", () => receiveMessage(friend, publicChannel, $"{API.LocalUser.Value.Username.ToUpperInvariant()} has been reading this"));
AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0);
}
[Test]
public void TestNoNotificationOnSelfMention()
{
AddStep("switch to PM channel", () => testContainer.ChannelManager.CurrentChannel.Value = privateMessageChannel);
AddStep("receive self-mention", () => receiveMessage(API.LocalUser.Value, publicChannel, $"my name is {API.LocalUser.Value.Username}"));
AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0);
}
[Test]
public void TestNoNotificationOnPMFromSelf()
{
AddStep("switch to public channel", () => testContainer.ChannelManager.CurrentChannel.Value = publicChannel);
AddStep("receive PM from self", () => receiveMessage(API.LocalUser.Value, privateMessageChannel, "hey hey"));
AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0);
}
[Test]
public void TestNotificationsNotFiredTwice()
{
AddStep("switch to public channel", () => testContainer.ChannelManager.CurrentChannel.Value = publicChannel);
AddStep("receive same PM twice", () =>
{
var message = createMessage(friend, privateMessageChannel, "hey hey");
privateMessageChannel.AddNewMessages(message, message);
});
AddStep("open notification overlay", () => testContainer.NotificationOverlay.Show());
AddAssert("1 notification fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 1);
}
private void receiveMessage(User sender, Channel channel, string content) => channel.AddNewMessages(createMessage(sender, channel, content));
private Message createMessage(User sender, Channel channel, string content) => new Message(messageIdCounter++)
{
Content = content,
Sender = sender,
ChannelId = channel.Id
};
private void clickNotification<T>() where T : Notification
{
var notification = testContainer.NotificationOverlay.ChildrenOfType<T>().Single();
InputManager.MoveMouseTo(notification);
InputManager.Click(MouseButton.Left);
}
private class TestContainer : Container
{
[Cached]
public ChannelManager ChannelManager { get; } = new ChannelManager();
[Cached]
public NotificationOverlay NotificationOverlay { get; } = new NotificationOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
};
[Cached]
public ChatOverlay ChatOverlay { get; } = new ChatOverlay();
private readonly MessageNotifier messageNotifier = new MessageNotifier();
private readonly Channel[] channels;
public TestContainer(Channel[] channels)
{
this.channels = channels;
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
ChannelManager,
ChatOverlay,
NotificationOverlay,
messageNotifier,
};
((BindableList<Channel>)ChannelManager.AvailableChannels).AddRange(channels);
foreach (var channel in channels)
ChannelManager.JoinChannel(channel);
}
}
}
}

View File

@ -2,15 +2,22 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK;
@ -23,32 +30,98 @@ namespace osu.Game.Tests.Visual.SongSelect
[Cached]
private readonly DialogOverlay dialogOverlay;
private ScoreManager scoreManager;
private RulesetStore rulesetStore;
private BeatmapManager beatmapManager;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory));
return dependencies;
}
public TestSceneBeatmapLeaderboard()
{
Add(dialogOverlay = new DialogOverlay
AddRange(new Drawable[]
{
Depth = -1
dialogOverlay = new DialogOverlay
{
Depth = -1
},
leaderboard = new FailableLeaderboard
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(550f, 450f),
Scope = BeatmapLeaderboardScope.Global,
}
});
}
[Test]
public void TestLocalScoresDisplay()
{
BeatmapInfo beatmapInfo = null;
AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local);
AddStep(@"Set beatmap", () =>
{
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
leaderboard.Beatmap = beatmapInfo;
});
Add(leaderboard = new FailableLeaderboard
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(550f, 450f),
Scope = BeatmapLeaderboardScope.Global,
});
clearScores();
checkCount(0);
AddStep(@"New Scores", newScores);
loadMoreScores(() => beatmapInfo);
checkCount(10);
loadMoreScores(() => beatmapInfo);
checkCount(20);
clearScores();
checkCount(0);
}
[Test]
public void TestGlobalScoresDisplay()
{
AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global);
AddStep(@"New Scores", () => leaderboard.Scores = generateSampleScores(null));
}
[Test]
public void TestPersonalBest()
{
AddStep(@"Show personal best", showPersonalBest);
AddStep("null personal best position", showPersonalBestWithNullPosition);
}
[Test]
public void TestPlaceholderStates()
{
AddStep(@"Empty Scores", () => leaderboard.SetRetrievalState(PlaceholderState.NoScores));
AddStep(@"Network failure", () => leaderboard.SetRetrievalState(PlaceholderState.NetworkFailure));
AddStep(@"No supporter", () => leaderboard.SetRetrievalState(PlaceholderState.NotSupporter));
AddStep(@"Not logged in", () => leaderboard.SetRetrievalState(PlaceholderState.NotLoggedIn));
AddStep(@"Unavailable", () => leaderboard.SetRetrievalState(PlaceholderState.Unavailable));
AddStep(@"None selected", () => leaderboard.SetRetrievalState(PlaceholderState.NoneSelected));
}
[Test]
public void TestBeatmapStates()
{
foreach (BeatmapSetOnlineStatus status in Enum.GetValues(typeof(BeatmapSetOnlineStatus)))
AddStep($"{status} beatmap", () => showBeatmapWithStatus(status));
AddStep("null personal best position", showPersonalBestWithNullPosition);
}
private void showPersonalBestWithNullPosition()
@ -96,9 +169,26 @@ namespace osu.Game.Tests.Visual.SongSelect
};
}
private void newScores()
private void loadMoreScores(Func<BeatmapInfo> beatmapInfo)
{
var scores = new[]
AddStep(@"Load new scores via manager", () =>
{
foreach (var score in generateSampleScores(beatmapInfo()))
scoreManager.Import(score).Wait();
});
}
private void clearScores()
{
AddStep("Clear all scores", () => scoreManager.Delete(scoreManager.GetAllUsableScores()));
}
private void checkCount(int expected) =>
AddUntilStep("Correct count displayed", () => leaderboard.ChildrenOfType<LeaderboardScore>().Count() == expected);
private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmap)
{
return new[]
{
new ScoreInfo
{
@ -107,6 +197,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Beatmap = beatmap,
User = new User
{
Id = 6602580,
@ -125,6 +216,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Beatmap = beatmap,
User = new User
{
Id = 4608074,
@ -143,6 +235,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Beatmap = beatmap,
User = new User
{
Id = 1014222,
@ -161,6 +254,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Beatmap = beatmap,
User = new User
{
Id = 1541390,
@ -179,6 +273,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Beatmap = beatmap,
User = new User
{
Id = 2243452,
@ -197,6 +292,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Beatmap = beatmap,
User = new User
{
Id = 2705430,
@ -215,6 +311,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Beatmap = beatmap,
User = new User
{
Id = 7151382,
@ -233,6 +330,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Beatmap = beatmap,
User = new User
{
Id = 2051389,
@ -251,6 +349,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Beatmap = beatmap,
User = new User
{
Id = 6169483,
@ -269,6 +368,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Beatmap = beatmap,
User = new User
{
Id = 6702666,
@ -281,8 +381,6 @@ namespace osu.Game.Tests.Visual.SongSelect
},
},
};
leaderboard.Scores = scores;
}
private void showBeatmapWithStatus(BeatmapSetOnlineStatus status)

View File

@ -61,6 +61,9 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.ShowOnlineExplicitContent, false);
SetDefault(OsuSetting.NotifyOnUsernameMentioned, true);
SetDefault(OsuSetting.NotifyOnPrivateMessage, true);
// Audio
SetDefault(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01);
@ -259,6 +262,8 @@ namespace osu.Game.Configuration
ScalingSizeY,
UIScale,
IntroSequence,
NotifyOnUsernameMentioned,
NotifyOnPrivateMessage,
UIHoldActivationDelay,
HitLighting,
MenuBackgroundSource,

View File

@ -1,20 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.ComponentModel;
namespace osu.Game.Configuration
{
public enum RankingType
{
Local,
[Description("Global")]
Top,
[Description("Selected Mods")]
SelectedMod,
Friends,
Country
}
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Graphics.Containers
protected virtual HoverClickSounds CreateHoverClickSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet);
public OsuClickableContainer(HoverSampleSet sampleSet = HoverSampleSet.Normal)
public OsuClickableContainer(HoverSampleSet sampleSet = HoverSampleSet.Default)
{
this.sampleSet = sampleSet;
}

View File

@ -107,10 +107,10 @@ namespace osu.Game.Graphics.Containers
{
}
private bool playedPopInSound;
protected override void UpdateState(ValueChangedEvent<Visibility> state)
{
bool didChange = state.NewValue != state.OldValue;
switch (state.NewValue)
{
case Visibility.Visible:
@ -121,18 +121,15 @@ namespace osu.Game.Graphics.Containers
return;
}
samplePopIn?.Play();
playedPopInSound = true;
if (didChange)
samplePopIn?.Play();
if (BlockScreenWideMouse && DimMainContent) game?.AddBlockingOverlay(this);
break;
case Visibility.Hidden:
if (playedPopInSound)
{
if (didChange)
samplePopOut?.Play();
playedPopInSound = false;
}
if (BlockScreenWideMouse) game?.RemoveBlockingOverlay(this);
break;

View File

@ -3,7 +3,6 @@
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -23,9 +22,6 @@ namespace osu.Game.Graphics.UserInterface
private const int text_size = 17;
private const int transition_length = 80;
private Sample sampleClick;
private Sample sampleHover;
private TextContainer text;
public DrawableOsuMenuItem(MenuItem item)
@ -36,12 +32,11 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
sampleHover = audio.Samples.Get(@"UI/generic-hover");
sampleClick = audio.Samples.Get(@"UI/generic-select");
BackgroundColour = Color4.Transparent;
BackgroundColourHover = Color4Extensions.FromHex(@"172023");
AddInternal(new HoverClickSounds());
updateTextColour();
Item.Action.BindDisabledChanged(_ => updateState(), true);
@ -84,7 +79,6 @@ namespace osu.Game.Graphics.UserInterface
if (IsHovered && !Item.Action.Disabled)
{
sampleHover.Play();
text.BoldText.FadeIn(transition_length, Easing.OutQuint);
text.NormalText.FadeOut(transition_length, Easing.OutQuint);
}
@ -95,12 +89,6 @@ namespace osu.Game.Graphics.UserInterface
}
}
protected override bool OnClick(ClickEvent e)
{
sampleClick.Play();
return base.OnClick(e);
}
protected sealed override Drawable CreateContent() => text = CreateTextContainer();
protected virtual TextContainer CreateTextContainer() => new TextContainer();

View File

@ -28,7 +28,7 @@ namespace osu.Game.Graphics.UserInterface
/// Array of button codes which should trigger the click sound.
/// If this optional parameter is omitted or set to <code>null</code>, the click sound will only be played on left click.
/// </param>
public HoverClickSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal, MouseButton[] buttons = null)
public HoverClickSounds(HoverSampleSet sampleSet = HoverSampleSet.Default, MouseButton[] buttons = null)
: base(sampleSet)
{
this.buttons = buttons ?? new[] { MouseButton.Left };
@ -45,7 +45,8 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
sampleClick = audio.Samples.Get($@"UI/generic-select{SampleSet.GetDescription()}");
sampleClick = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select")
?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select");
}
}
}

View File

@ -0,0 +1,25 @@
// 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.ComponentModel;
namespace osu.Game.Graphics.UserInterface
{
public enum HoverSampleSet
{
[Description("default")]
Default,
[Description("button")]
Button,
[Description("softer")]
Soft,
[Description("toolbar")]
Toolbar,
[Description("songselect")]
SongSelect
}
}

View File

@ -1,7 +1,6 @@
// 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.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@ -22,7 +21,7 @@ namespace osu.Game.Graphics.UserInterface
protected readonly HoverSampleSet SampleSet;
public HoverSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal)
public HoverSounds(HoverSampleSet sampleSet = HoverSampleSet.Default)
{
SampleSet = sampleSet;
RelativeSizeAxes = Axes.Both;
@ -31,7 +30,8 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(AudioManager audio, SessionStatics statics)
{
sampleHover = audio.Samples.Get($@"UI/generic-hover{SampleSet.GetDescription()}");
sampleHover = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-hover")
?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-hover");
}
public override void PlayHoverSample()
@ -40,22 +40,4 @@ namespace osu.Game.Graphics.UserInterface
sampleHover.Play();
}
}
public enum HoverSampleSet
{
[Description("")]
Loud,
[Description("-soft")]
Normal,
[Description("-softer")]
Soft,
[Description("-toolbar")]
Toolbar,
[Description("-songselect")]
SongSelect
}
}

View File

@ -44,6 +44,7 @@ namespace osu.Game.Graphics.UserInterface
private readonly Box hover;
public OsuAnimatedButton()
: base(HoverSampleSet.Button)
{
base.Content.Add(content = new Container
{

View File

@ -49,7 +49,7 @@ namespace osu.Game.Graphics.UserInterface
protected Box Background;
protected SpriteText SpriteText;
public OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Loud)
public OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Button)
{
Height = 40;

View File

@ -7,17 +7,17 @@ namespace osu.Game.Localisation
{
public static class ChatStrings
{
private const string prefix = "osu.Game.Localisation.Chat";
private const string prefix = @"osu.Game.Localisation.Chat";
/// <summary>
/// "chat"
/// </summary>
public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "chat");
public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"chat");
/// <summary>
/// "join the real-time discussion"
/// </summary>
public static LocalisableString HeaderDescription => new TranslatableString(getKey("header_description"), "join the real-time discussion");
public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"join the real-time discussion");
private static string getKey(string key) => $"{prefix}:{key}";
}

View File

@ -7,12 +7,12 @@ namespace osu.Game.Localisation
{
public static class CommonStrings
{
private const string prefix = "osu.Game.Localisation.Common";
private const string prefix = @"osu.Game.Localisation.Common";
/// <summary>
/// "Cancel"
/// </summary>
public static LocalisableString Cancel => new TranslatableString(getKey("cancel"), "Cancel");
public static LocalisableString Cancel => new TranslatableString(getKey(@"cancel"), @"Cancel");
private static string getKey(string key) => $"{prefix}:{key}";
}

View File

@ -7,10 +7,10 @@ namespace osu.Game.Localisation
{
public enum Language
{
[Description("English")]
[Description(@"English")]
en,
[Description("日本語")]
[Description(@"日本語")]
ja
}
}

View File

@ -7,17 +7,17 @@ namespace osu.Game.Localisation
{
public static class NotificationsStrings
{
private const string prefix = "osu.Game.Localisation.Notifications";
private const string prefix = @"osu.Game.Localisation.Notifications";
/// <summary>
/// "notifications"
/// </summary>
public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "notifications");
public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"notifications");
/// <summary>
/// "waiting for 'ya"
/// </summary>
public static LocalisableString HeaderDescription => new TranslatableString(getKey("header_description"), "waiting for 'ya");
public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"waiting for 'ya");
private static string getKey(string key) => $"{prefix}:{key}";
}

View File

@ -7,17 +7,17 @@ namespace osu.Game.Localisation
{
public static class NowPlayingStrings
{
private const string prefix = "osu.Game.Localisation.NowPlaying";
private const string prefix = @"osu.Game.Localisation.NowPlaying";
/// <summary>
/// "now playing"
/// </summary>
public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "now playing");
public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"now playing");
/// <summary>
/// "manage the currently playing track"
/// </summary>
public static LocalisableString HeaderDescription => new TranslatableString(getKey("header_description"), "manage the currently playing track");
public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"manage the currently playing track");
private static string getKey(string key) => $"{prefix}:{key}";
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Resources;
using System.Threading.Tasks;
using osu.Framework.Localisation;
@ -34,7 +35,29 @@ namespace osu.Game.Localisation
lock (resourceManagers)
{
if (!resourceManagers.TryGetValue(ns, out var manager))
resourceManagers[ns] = manager = new ResourceManager(ns, GetType().Assembly);
{
var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
// Traverse backwards through periods in the namespace to find a matching assembly.
string assemblyName = ns;
while (!string.IsNullOrEmpty(assemblyName))
{
var matchingAssembly = loadedAssemblies.FirstOrDefault(asm => asm.GetName().Name == assemblyName);
if (matchingAssembly != null)
{
resourceManagers[ns] = manager = new ResourceManager(ns, matchingAssembly);
break;
}
int lastIndex = Math.Max(0, assemblyName.LastIndexOf('.'));
assemblyName = assemblyName.Substring(0, lastIndex);
}
}
if (manager == null)
return null;
try
{

View File

@ -7,17 +7,17 @@ namespace osu.Game.Localisation
{
public static class SettingsStrings
{
private const string prefix = "osu.Game.Localisation.Settings";
private const string prefix = @"osu.Game.Localisation.Settings";
/// <summary>
/// "settings"
/// </summary>
public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "settings");
public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"settings");
/// <summary>
/// "change the way osu! behaves"
/// </summary>
public static LocalisableString HeaderDescription => new TranslatableString(getKey("header_description"), "change the way osu! behaves");
public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"change the way osu! behaves");
private static string getKey(string key) => $"{prefix}:{key}";
}

View File

@ -11,11 +11,11 @@ namespace osu.Game.Online.API.Requests
{
public class CreateChannelRequest : APIRequest<APIChatChannel>
{
private readonly Channel channel;
public readonly Channel Channel;
public CreateChannelRequest(Channel channel)
{
this.channel = channel;
Channel = channel;
}
protected override WebRequest CreateWebRequest()
@ -24,7 +24,7 @@ namespace osu.Game.Online.API.Requests
req.Method = HttpMethod.Post;
req.AddParameter("type", $"{ChannelType.PM}");
req.AddParameter("target_id", $"{channel.Users.First().Id}");
req.AddParameter("target_id", $"{Channel.Users.First().Id}");
return req;
}

View File

@ -63,5 +63,7 @@ namespace osu.Game.Online.Chat
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => $"[{ChannelId}] ({Id}) {Sender}: {Content}";
}
}

View File

@ -0,0 +1,181 @@
// 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 System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Users;
namespace osu.Game.Online.Chat
{
/// <summary>
/// Component that handles creating and posting notifications for incoming messages.
/// </summary>
public class MessageNotifier : Component
{
[Resolved]
private NotificationOverlay notifications { get; set; }
[Resolved]
private ChatOverlay chatOverlay { get; set; }
[Resolved]
private ChannelManager channelManager { get; set; }
private Bindable<bool> notifyOnUsername;
private Bindable<bool> notifyOnPrivateMessage;
private readonly IBindable<User> localUser = new Bindable<User>();
private readonly IBindableList<Channel> joinedChannels = new BindableList<Channel>();
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, IAPIProvider api)
{
notifyOnUsername = config.GetBindable<bool>(OsuSetting.NotifyOnUsernameMentioned);
notifyOnPrivateMessage = config.GetBindable<bool>(OsuSetting.NotifyOnPrivateMessage);
localUser.BindTo(api.LocalUser);
joinedChannels.BindTo(channelManager.JoinedChannels);
}
protected override void LoadComplete()
{
base.LoadComplete();
joinedChannels.BindCollectionChanged(channelsChanged, true);
}
private void channelsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var channel in e.NewItems.Cast<Channel>())
channel.NewMessagesArrived += checkNewMessages;
break;
case NotifyCollectionChangedAction.Remove:
foreach (var channel in e.OldItems.Cast<Channel>())
channel.NewMessagesArrived -= checkNewMessages;
break;
}
}
private void checkNewMessages(IEnumerable<Message> messages)
{
if (!messages.Any())
return;
var channel = channelManager.JoinedChannels.SingleOrDefault(c => c.Id == messages.First().ChannelId);
if (channel == null)
return;
// Only send notifications, if ChatOverlay and the target channel aren't visible.
if (chatOverlay.IsPresent && channelManager.CurrentChannel.Value == channel)
return;
foreach (var message in messages.OrderByDescending(m => m.Id))
{
// ignore messages that already have been read
if (message.Id <= channel.LastReadId)
return;
if (message.Sender.Id == localUser.Value.Id)
continue;
// check for private messages first to avoid both posting two notifications about the same message
if (checkForPMs(channel, message))
continue;
checkForMentions(channel, message);
}
}
/// <summary>
/// Checks whether the user enabled private message notifications and whether specified <paramref name="message"/> is a direct message.
/// </summary>
/// <param name="channel">The channel associated to the <paramref name="message"/></param>
/// <param name="message">The message to be checked</param>
/// <returns>Whether a notification was fired.</returns>
private bool checkForPMs(Channel channel, Message message)
{
if (!notifyOnPrivateMessage.Value || channel.Type != ChannelType.PM)
return false;
notifications.Post(new PrivateMessageNotification(message.Sender.Username, channel));
return true;
}
private void checkForMentions(Channel channel, Message message)
{
if (!notifyOnUsername.Value || !checkContainsUsername(message.Content, localUser.Value.Username)) return;
notifications.Post(new MentionNotification(message.Sender.Username, channel));
}
/// <summary>
/// Checks if <paramref name="message"/> contains <paramref name="username"/>.
/// This will match against the case where underscores are used instead of spaces (which is how osu-stable handles usernames with spaces).
/// </summary>
private static bool checkContainsUsername(string message, string username) => message.Contains(username, StringComparison.OrdinalIgnoreCase) || message.Contains(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase);
public class PrivateMessageNotification : OpenChannelNotification
{
public PrivateMessageNotification(string username, Channel channel)
: base(channel)
{
Icon = FontAwesome.Solid.Envelope;
Text = $"You received a private message from '{username}'. Click to read it!";
}
}
public class MentionNotification : OpenChannelNotification
{
public MentionNotification(string username, Channel channel)
: base(channel)
{
Icon = FontAwesome.Solid.At;
Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!";
}
}
public abstract class OpenChannelNotification : SimpleNotification
{
protected OpenChannelNotification(Channel channel)
{
this.channel = channel;
}
private readonly Channel channel;
public override bool IsImportant => false;
[BackgroundDependencyLoader]
private void load(OsuColour colours, ChatOverlay chatOverlay, NotificationOverlay notificationOverlay, ChannelManager channelManager)
{
IconBackgound.Colour = colours.PurpleDark;
Activated = delegate
{
notificationOverlay.Hide();
chatOverlay.Show();
channelManager.CurrentChannel.Value = channel;
return true;
};
}
}
}
}

View File

@ -44,9 +44,9 @@ namespace osu.Game.Online.Leaderboards
protected override Container<Drawable> Content => content;
private IEnumerable<TScoreInfo> scores;
private ICollection<TScoreInfo> scores;
public IEnumerable<TScoreInfo> Scores
public ICollection<TScoreInfo> Scores
{
get => scores;
set
@ -126,7 +126,7 @@ namespace osu.Game.Online.Leaderboards
return;
scope = value;
UpdateScores();
RefreshScores();
}
}
@ -154,7 +154,7 @@ namespace osu.Game.Online.Leaderboards
case PlaceholderState.NetworkFailure:
replacePlaceholder(new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync)
{
Action = UpdateScores,
Action = RefreshScores
});
break;
@ -254,8 +254,6 @@ namespace osu.Game.Online.Leaderboards
apiState.BindValueChanged(onlineStateChanged, true);
}
public void RefreshScores() => UpdateScores();
private APIRequest getScoresRequest;
protected abstract bool IsOnlineScope { get; }
@ -267,12 +265,14 @@ namespace osu.Game.Online.Leaderboards
case APIState.Online:
case APIState.Offline:
if (IsOnlineScope)
UpdateScores();
RefreshScores();
break;
}
});
public void RefreshScores() => Scheduler.AddOnce(UpdateScores);
protected void UpdateScores()
{
// don't display any scores or placeholder until the first Scores_Set has been called.
@ -290,7 +290,7 @@ namespace osu.Game.Online.Leaderboards
getScoresRequest = FetchScores(scores => Schedule(() =>
{
Scores = scores;
Scores = scores.ToArray();
PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores;
}));

View File

@ -728,6 +728,7 @@ namespace osu.Game
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
loadComponentSingleFile(new MessageNotifier(), AddInternal, true);
loadComponentSingleFile(Settings = new SettingsOverlay { GetToolbarHeight = () => ToolbarOffset }, leftFloatingOverlayContent.Add, true);
var changelogOverlay = loadComponentSingleFile(new ChangelogOverlay(), overlayContent.Add, true);
loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true);

View File

@ -8,7 +8,6 @@ using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Input.Bindings;
@ -25,8 +24,6 @@ namespace osu.Game.Overlays
public readonly Bindable<APIChangelogBuild> Current = new Bindable<APIChangelogBuild>();
private Sample sampleBack;
private List<APIChangelogBuild> builds;
protected List<APIUpdateStream> Streams;
@ -41,8 +38,6 @@ namespace osu.Game.Overlays
{
Header.Build.BindTarget = Current;
sampleBack = audio.Samples.Get(@"UI/generic-select-soft");
Current.BindValueChanged(e =>
{
if (e.NewValue != null)
@ -108,7 +103,6 @@ namespace osu.Game.Overlays
else
{
Current.Value = null;
sampleBack?.Play();
}
return true;

View File

@ -6,18 +6,18 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Online.Chat;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
using osuTK.Graphics;
namespace osu.Game.Overlays.Chat
{

View File

@ -0,0 +1,32 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Game.Configuration;
namespace osu.Game.Overlays.Settings.Sections.Online
{
public class AlertsAndPrivacySettings : SettingsSubsection
{
protected override string Header => "Alerts and Privacy";
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
Children = new Drawable[]
{
new SettingsCheckbox
{
LabelText = "Show a notification when someone mentions your name",
Current = config.GetBindable<bool>(OsuSetting.NotifyOnUsernameMentioned)
},
new SettingsCheckbox
{
LabelText = "Show a notification when you receive a private message",
Current = config.GetBindable<bool>(OsuSetting.NotifyOnPrivateMessage)
},
};
}
}
}

View File

@ -21,6 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections
Children = new Drawable[]
{
new WebSettings(),
new AlertsAndPrivacySettings(),
new IntegrationSettings()
};
}

View File

@ -44,6 +44,8 @@ namespace osu.Game.Screens.Select.Leaderboards
private IBindable<WeakReference<ScoreInfo>> itemRemoved;
private IBindable<WeakReference<ScoreInfo>> itemAdded;
/// <summary>
/// Whether to apply the game's currently selected mods as a filter when retrieving scores.
/// </summary>
@ -85,6 +87,9 @@ namespace osu.Game.Screens.Select.Leaderboards
itemRemoved = scoreManager.ItemRemoved.GetBoundCopy();
itemRemoved.BindValueChanged(onScoreRemoved);
itemAdded = scoreManager.ItemUpdated.GetBoundCopy();
itemAdded.BindValueChanged(onScoreAdded);
}
protected override void Reset()
@ -93,7 +98,25 @@ namespace osu.Game.Screens.Select.Leaderboards
TopScore = null;
}
private void onScoreRemoved(ValueChangedEvent<WeakReference<ScoreInfo>> score) => Schedule(RefreshScores);
private void onScoreRemoved(ValueChangedEvent<WeakReference<ScoreInfo>> score) =>
scoreStoreChanged(score);
private void onScoreAdded(ValueChangedEvent<WeakReference<ScoreInfo>> score) =>
scoreStoreChanged(score);
private void scoreStoreChanged(ValueChangedEvent<WeakReference<ScoreInfo>> score)
{
if (Scope != BeatmapLeaderboardScope.Local)
return;
if (score.NewValue.TryGetTarget(out var scoreInfo))
{
if (Beatmap?.ID != scoreInfo.BeatmapInfoID)
return;
}
RefreshScores();
}
protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local;

View File

@ -148,9 +148,9 @@ namespace osu.Game.Skinning
}
}
internal class LegacyOldStyleFill : LegacyHealthPiece
internal abstract class LegacyFill : LegacyHealthPiece
{
public LegacyOldStyleFill(ISkin skin)
protected LegacyFill(ISkin skin)
{
// required for sizing correctly..
var firstFrame = getTexture(skin, "colour-0");
@ -162,27 +162,29 @@ namespace osu.Game.Skinning
}
else
{
InternalChild = skin.GetAnimation("scorebar-colour", true, true, startAtCurrentTime: false, applyConfigFrameRate: true) ?? Drawable.Empty();
InternalChild = skin.GetAnimation("scorebar-colour", true, true, startAtCurrentTime: false, applyConfigFrameRate: true) ?? Empty();
Size = new Vector2(firstFrame.DisplayWidth, firstFrame.DisplayHeight);
}
Position = new Vector2(3, 10) * 1.6f;
Masking = true;
}
}
internal class LegacyNewStyleFill : LegacyHealthPiece
internal class LegacyOldStyleFill : LegacyFill
{
public LegacyOldStyleFill(ISkin skin)
: base(skin)
{
Position = new Vector2(3, 10) * 1.6f;
}
}
internal class LegacyNewStyleFill : LegacyFill
{
public LegacyNewStyleFill(ISkin skin)
: base(skin)
{
InternalChild = new Sprite
{
Texture = getTexture(skin, "colour"),
};
Size = InternalChild.Size;
Position = new Vector2(7.5f, 7.8f) * 1.6f;
Masking = true;
}
protected override void Update()

View File

@ -4,7 +4,6 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Framework.Testing.Input;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
@ -49,7 +48,7 @@ namespace osu.Game.Tests.Visual
InputManager = new ManualInputManager
{
UseParentInput = true,
Child = new PlatformActionContainer().WithChild(mainContent)
Child = mainContent
},
new Container
{

View File

@ -30,12 +30,12 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.LocalisationAnalyser" Version="2021.525.0">
<PackageReference Include="ppy.LocalisationAnalyser" Version="2021.608.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="ppy.osu.Framework" Version="2021.609.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.604.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.614.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.611.0" />
<PackageReference Include="Sentry" Version="3.4.0" />
<PackageReference Include="SharpCompress" Version="0.28.2" />
<PackageReference Include="NUnit" Version="3.13.2" />

View File

@ -70,8 +70,8 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.609.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.604.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.614.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.611.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
<PropertyGroup>
@ -93,7 +93,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.609.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.614.0" />
<PackageReference Include="SharpCompress" Version="0.28.2" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" />