1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-14 02:13:21 +08:00

Merge branch 'master' into activity-on-multiplayer-screens

This commit is contained in:
Bartłomiej Dach 2021-08-24 18:50:55 +02:00
commit e503182a8d
No known key found for this signature in database
GPG Key ID: BCECCD4FA41F6497
28 changed files with 567 additions and 130 deletions

View File

@ -9,6 +9,8 @@ namespace osu.Game.Tests.Resources
{ {
public static class TestResources public static class TestResources
{ {
public const double QUICK_BEATMAP_LENGTH = 10000;
public static DllResourceStore GetStore() => new DllResourceStore(typeof(TestResources).Assembly); public static DllResourceStore GetStore() => new DllResourceStore(typeof(TestResources).Assembly);
public static Stream OpenResource(string name) => GetStore().GetStream($"Resources/{name}"); public static Stream OpenResource(string name) => GetStore().GetStream($"Resources/{name}");

View File

@ -138,16 +138,42 @@ namespace osu.Game.Tests.Skins.IO
} }
} }
private MemoryStream createOsk(string name, string author) [Test]
public async Task TestSameMetadataNameDifferentFolderName()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
{
try
{
var osu = LoadOsuIntoHost(host);
var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1", false), "my custom skin 1"));
Assert.That(imported.Name, Is.EqualTo("name 1 [my custom skin 1]"));
Assert.That(imported.Creator, Is.EqualTo("author 1"));
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1", false), "my custom skin 2"));
Assert.That(imported2.Name, Is.EqualTo("name 1 [my custom skin 2]"));
Assert.That(imported2.Creator, Is.EqualTo("author 1"));
Assert.That(imported2.Hash, Is.Not.EqualTo(imported.Hash));
}
finally
{
host.Exit();
}
}
}
private MemoryStream createOsk(string name, string author, bool makeUnique = true)
{ {
var zipStream = new MemoryStream(); var zipStream = new MemoryStream();
using var zip = ZipArchive.Create(); using var zip = ZipArchive.Create();
zip.AddEntry("skin.ini", generateSkinIni(name, author)); zip.AddEntry("skin.ini", generateSkinIni(name, author, makeUnique));
zip.SaveTo(zipStream); zip.SaveTo(zipStream);
return zipStream; return zipStream;
} }
private MemoryStream generateSkinIni(string name, string author) private MemoryStream generateSkinIni(string name, string author, bool makeUnique = true)
{ {
var stream = new MemoryStream(); var stream = new MemoryStream();
var writer = new StreamWriter(stream); var writer = new StreamWriter(stream);
@ -155,8 +181,12 @@ namespace osu.Game.Tests.Skins.IO
writer.WriteLine("[General]"); writer.WriteLine("[General]");
writer.WriteLine($"Name: {name}"); writer.WriteLine($"Name: {name}");
writer.WriteLine($"Author: {author}"); writer.WriteLine($"Author: {author}");
writer.WriteLine();
writer.WriteLine($"# unique {Guid.NewGuid()}"); if (makeUnique)
{
writer.WriteLine();
writer.WriteLine($"# unique {Guid.NewGuid()}");
}
writer.Flush(); writer.Flush();

View File

@ -142,7 +142,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("set hud to never show", () => localConfig.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never)); AddStep("set hud to never show", () => localConfig.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never));
createNew(); createNew();
AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded); AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded);
AddUntilStep("wait for components to be hidden", () => !hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().IsPresent);
AddStep("reload components", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().Reload());
AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().ComponentsLoaded); AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().ComponentsLoaded);
} }

View File

@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
base.SetUpSteps(); base.SetUpSteps();
AddStep("load chat display", () => Child = chatDisplay = new GameplayChatDisplay AddStep("load chat display", () => Child = chatDisplay = new GameplayChatDisplay(SelectedRoom.Value)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -28,6 +28,8 @@ using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osu.Game.Users; using osu.Game.Users;
using osuTK.Input; using osuTK.Input;
@ -430,6 +432,40 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
} }
[Test]
public void TestGameplayFlow()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddRepeatStep("click spectate button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerReadyButton>().Single());
InputManager.Click(MouseButton.Left);
}, 2);
AddUntilStep("wait for player", () => Stack.CurrentScreen is Player);
// Gameplay runs in real-time, so we need to incrementally check if gameplay has finished in order to not time out.
for (double i = 1000; i < TestResources.QUICK_BEATMAP_LENGTH; i += 1000)
{
var time = i;
AddUntilStep($"wait for time > {i}", () => this.ChildrenOfType<GameplayClockContainer>().SingleOrDefault()?.GameplayClock.CurrentTime > time);
}
AddUntilStep("wait for results", () => Stack.CurrentScreen is ResultsScreen);
}
private void createRoom(Func<Room> room) private void createRoom(Func<Room> room)
{ {
AddUntilStep("wait for lounge", () => multiplayerScreen.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true); AddUntilStep("wait for lounge", () => multiplayerScreen.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("initialise gameplay", () => AddStep("initialise gameplay", () =>
{ {
Stack.Push(player = new MultiplayerPlayer(Client.CurrentMatchPlayingItem.Value, Client.Room?.Users.ToArray())); Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, Client.CurrentMatchPlayingItem.Value, Client.Room?.Users.ToArray()));
}); });
} }

View File

@ -42,19 +42,21 @@ namespace osu.Game.Tests.Visual.Online
() => commentsContainer.ChildrenOfType<CommentsShowMoreButton>().Single().IsLoading); () => commentsContainer.ChildrenOfType<CommentsShowMoreButton>().Single().IsLoading);
} }
[Test] [TestCase(false)]
public void TestSingleCommentsPage() [TestCase(true)]
public void TestSingleCommentsPage(bool withPinned)
{ {
setUpCommentsResponse(exampleComments); setUpCommentsResponse(getExampleComments(withPinned));
AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123)); AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123));
AddUntilStep("show more button hidden", AddUntilStep("show more button hidden",
() => commentsContainer.ChildrenOfType<CommentsShowMoreButton>().Single().Alpha == 0); () => commentsContainer.ChildrenOfType<CommentsShowMoreButton>().Single().Alpha == 0);
} }
[Test] [TestCase(false)]
public void TestMultipleCommentPages() [TestCase(true)]
public void TestMultipleCommentPages(bool withPinned)
{ {
var comments = exampleComments; var comments = getExampleComments(withPinned);
comments.HasMore = true; comments.HasMore = true;
comments.TopLevelCount = 10; comments.TopLevelCount = 10;
@ -64,11 +66,12 @@ namespace osu.Game.Tests.Visual.Online
() => commentsContainer.ChildrenOfType<CommentsShowMoreButton>().Single().Alpha == 1); () => commentsContainer.ChildrenOfType<CommentsShowMoreButton>().Single().Alpha == 1);
} }
[Test] [TestCase(false)]
public void TestMultipleLoads() [TestCase(true)]
public void TestMultipleLoads(bool withPinned)
{ {
var comments = exampleComments; var comments = getExampleComments(withPinned);
int topLevelCommentCount = exampleComments.Comments.Count; int topLevelCommentCount = comments.Comments.Count;
AddStep("hide container", () => commentsContainer.Hide()); AddStep("hide container", () => commentsContainer.Hide());
setUpCommentsResponse(comments); setUpCommentsResponse(comments);
@ -79,6 +82,48 @@ namespace osu.Game.Tests.Visual.Online
() => commentsContainer.ChildrenOfType<DrawableComment>().Count() == topLevelCommentCount); () => commentsContainer.ChildrenOfType<DrawableComment>().Count() == topLevelCommentCount);
} }
[Test]
public void TestNoComment()
{
var comments = getExampleComments();
comments.Comments.Clear();
setUpCommentsResponse(comments);
AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123));
AddAssert("no comment shown", () => !commentsContainer.ChildrenOfType<DrawableComment>().Any());
}
[TestCase(false)]
[TestCase(true)]
public void TestSingleComment(bool withPinned)
{
var comment = new Comment
{
Id = 1,
Message = "This is a single comment",
LegacyName = "SingleUser",
CreatedAt = DateTimeOffset.Now,
VotesCount = 0,
Pinned = withPinned,
};
var bundle = new CommentBundle
{
Comments = new List<Comment> { comment },
IncludedComments = new List<Comment>(),
PinnedComments = new List<Comment>(),
};
if (withPinned)
bundle.PinnedComments.Add(comment);
setUpCommentsResponse(bundle);
AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123));
AddUntilStep("wait comment load", () => commentsContainer.ChildrenOfType<DrawableComment>().Any());
AddAssert("only one comment shown", () =>
commentsContainer.ChildrenOfType<DrawableComment>().Count(d => d.Comment.Pinned == withPinned) == 1);
}
private void setUpCommentsResponse(CommentBundle commentBundle) private void setUpCommentsResponse(CommentBundle commentBundle)
=> AddStep("set up response", () => => AddStep("set up response", () =>
{ {
@ -92,38 +137,71 @@ namespace osu.Game.Tests.Visual.Online
}; };
}); });
private CommentBundle exampleComments => new CommentBundle private CommentBundle getExampleComments(bool withPinned = false)
{ {
Comments = new List<Comment> var bundle = new CommentBundle
{ {
new Comment Comments = new List<Comment>
{ {
Id = 1, new Comment
Message = "This is a comment", {
LegacyName = "FirstUser", Id = 1,
CreatedAt = DateTimeOffset.Now, Message = "This is a comment",
VotesCount = 19, LegacyName = "FirstUser",
RepliesCount = 1 CreatedAt = DateTimeOffset.Now,
VotesCount = 19,
RepliesCount = 1
},
new Comment
{
Id = 5,
ParentId = 1,
Message = "This is a child comment",
LegacyName = "SecondUser",
CreatedAt = DateTimeOffset.Now,
VotesCount = 4,
},
new Comment
{
Id = 10,
Message = "This is another comment",
LegacyName = "ThirdUser",
CreatedAt = DateTimeOffset.Now,
VotesCount = 0
},
}, },
new Comment IncludedComments = new List<Comment>(),
PinnedComments = new List<Comment>(),
};
if (withPinned)
{
var pinnedComment = new Comment
{ {
Id = 5, Id = 15,
ParentId = 1, Message = "This is pinned comment",
Message = "This is a child comment", LegacyName = "PinnedUser",
LegacyName = "SecondUser",
CreatedAt = DateTimeOffset.Now, CreatedAt = DateTimeOffset.Now,
VotesCount = 4, VotesCount = 999,
}, Pinned = true,
new Comment RepliesCount = 1,
};
bundle.Comments.Add(pinnedComment);
bundle.PinnedComments.Add(pinnedComment);
bundle.Comments.Add(new Comment
{ {
Id = 10, Id = 20,
Message = "This is another comment", Message = "Reply to pinned comment",
LegacyName = "ThirdUser", LegacyName = "AbandonedUser",
CreatedAt = DateTimeOffset.Now, CreatedAt = DateTimeOffset.Now,
VotesCount = 0 VotesCount = 0,
}, ParentId = 15,
}, });
IncludedComments = new List<Comment>(), }
};
return bundle;
}
} }
} }

View File

@ -43,6 +43,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
AddStep(description, () => AddStep(description, () =>
{ {
comment.Pinned = description == "Pinned";
comment.Message = text; comment.Message = text;
container.Add(new DrawableComment(comment)); container.Add(new DrawableComment(comment));
}); });
@ -59,6 +60,7 @@ namespace osu.Game.Tests.Visual.Online
private static object[] comments = private static object[] comments =
{ {
new[] { "Plain", "This is plain comment" }, new[] { "Plain", "This is plain comment" },
new[] { "Pinned", "This is pinned comment" },
new[] { "Link", "Please visit https://osu.ppy.sh" }, new[] { "Link", "Please visit https://osu.ppy.sh" },
new[] new[]

View File

@ -149,6 +149,7 @@ namespace osu.Game.Tests.Visual.Online
} }
}, },
IncludedComments = new List<Comment>(), IncludedComments = new List<Comment>(),
PinnedComments = new List<Comment>(),
UserVotes = new List<long> UserVotes = new List<long>
{ {
5 5
@ -178,6 +179,7 @@ namespace osu.Game.Tests.Visual.Online
}, },
}, },
IncludedComments = new List<Comment>(), IncludedComments = new List<Comment>(),
PinnedComments = new List<Comment>(),
}; };
private class TestCommentsContainer : CommentsContainer private class TestCommentsContainer : CommentsContainer

View File

@ -806,7 +806,7 @@ namespace osu.Game.Database
protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash); protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
/// <summary> /// <summary>
/// Whether inport can be skipped after finding an existing import early in the process. /// Whether import can be skipped after finding an existing import early in the process.
/// Only valid when <see cref="ComputeHash"/> is not overridden. /// Only valid when <see cref="ComputeHash"/> is not overridden.
/// </summary> /// </summary>
/// <param name="existing">The existing model.</param> /// <param name="existing">The existing model.</param>

View File

@ -58,6 +58,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"edited_by_id")] [JsonProperty(@"edited_by_id")]
public long? EditedById { get; set; } public long? EditedById { get; set; }
[JsonProperty(@"pinned")]
public bool Pinned { get; set; }
public User EditedUser { get; set; } public User EditedUser { get; set; }
public bool IsTopLevel => !ParentId.HasValue; public bool IsTopLevel => !ParentId.HasValue;

View File

@ -4,6 +4,7 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Users; using osu.Game.Users;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Online.API.Requests.Responses namespace osu.Game.Online.API.Requests.Responses
{ {
@ -24,6 +25,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"included_comments")] [JsonProperty(@"included_comments")]
public List<Comment> IncludedComments { get; set; } public List<Comment> IncludedComments { get; set; }
[JsonProperty(@"pinned_comments")]
public List<Comment> PinnedComments { get; set; }
private List<long> userVotes; private List<long> userVotes;
[JsonProperty(@"user_votes")] [JsonProperty(@"user_votes")]
@ -49,26 +53,17 @@ namespace osu.Game.Online.API.Requests.Responses
{ {
users = value; users = value;
value.ForEach(u => foreach (var user in value)
{ {
Comments.ForEach(c => foreach (var comment in Comments.Concat(IncludedComments).Concat(PinnedComments))
{ {
if (c.UserId == u.Id) if (comment.UserId == user.Id)
c.User = u; comment.User = user;
if (c.EditedById == u.Id) if (comment.EditedById == user.Id)
c.EditedUser = u; comment.EditedUser = user;
}); }
}
IncludedComments.ForEach(c =>
{
if (c.UserId == u.Id)
c.User = u;
if (c.EditedById == u.Id)
c.EditedUser = u;
});
});
} }
} }

View File

@ -38,6 +38,7 @@ namespace osu.Game.Overlays.Comments
private CancellationTokenSource loadCancellation; private CancellationTokenSource loadCancellation;
private int currentPage; private int currentPage;
private FillFlowContainer pinnedContent;
private FillFlowContainer content; private FillFlowContainer content;
private DeletedCommentsCounter deletedCommentsCounter; private DeletedCommentsCounter deletedCommentsCounter;
private CommentsShowMoreButton moreButton; private CommentsShowMoreButton moreButton;
@ -63,6 +64,25 @@ namespace osu.Game.Overlays.Comments
Children = new Drawable[] Children = new Drawable[]
{ {
commentCounter = new TotalCommentsCounter(), commentCounter = new TotalCommentsCounter(),
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
pinnedContent = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
},
},
},
new CommentsHeader new CommentsHeader
{ {
Sort = { BindTarget = Sort }, Sort = { BindTarget = Sort },
@ -173,6 +193,7 @@ namespace osu.Game.Overlays.Comments
deletedCommentsCounter.Count.Value = 0; deletedCommentsCounter.Count.Value = 0;
moreButton.Show(); moreButton.Show();
moreButton.IsLoading = true; moreButton.IsLoading = true;
pinnedContent.Clear();
content.Clear(); content.Clear();
CommentDictionary.Clear(); CommentDictionary.Clear();
} }
@ -202,7 +223,7 @@ namespace osu.Game.Overlays.Comments
var topLevelComments = new List<DrawableComment>(); var topLevelComments = new List<DrawableComment>();
var orphaned = new List<Comment>(); var orphaned = new List<Comment>();
foreach (var comment in bundle.Comments.Concat(bundle.IncludedComments)) foreach (var comment in bundle.Comments.Concat(bundle.IncludedComments).Concat(bundle.PinnedComments))
{ {
// Exclude possible duplicated comments. // Exclude possible duplicated comments.
if (CommentDictionary.ContainsKey(comment.Id)) if (CommentDictionary.ContainsKey(comment.Id))
@ -219,13 +240,15 @@ namespace osu.Game.Overlays.Comments
{ {
LoadComponentsAsync(topLevelComments, loaded => LoadComponentsAsync(topLevelComments, loaded =>
{ {
content.AddRange(loaded); pinnedContent.AddRange(loaded.Where(d => d.Comment.Pinned));
content.AddRange(loaded.Where(d => !d.Comment.Pinned));
deletedCommentsCounter.Count.Value += topLevelComments.Select(d => d.Comment).Count(c => c.IsDeleted && c.IsTopLevel); deletedCommentsCounter.Count.Value += topLevelComments.Select(d => d.Comment).Count(c => c.IsDeleted && c.IsTopLevel);
if (bundle.HasMore) if (bundle.HasMore)
{ {
int loadedTopLevelComments = 0; int loadedTopLevelComments = 0;
pinnedContent.Children.OfType<DrawableComment>().ForEach(p => loadedTopLevelComments++);
content.Children.OfType<DrawableComment>().ForEach(p => loadedTopLevelComments++); content.Children.OfType<DrawableComment>().ForEach(p => loadedTopLevelComments++);
moreButton.Current.Value = bundle.TopLevelCount - loadedTopLevelComments; moreButton.Current.Value = bundle.TopLevelCount - loadedTopLevelComments;
@ -300,11 +323,6 @@ namespace osu.Game.Overlays.Comments
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4
},
new OsuSpriteText new OsuSpriteText
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,

View File

@ -21,6 +21,7 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using System.Collections.Specialized; using System.Collections.Specialized;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Overlays.Comments.Buttons; using osu.Game.Overlays.Comments.Buttons;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Comments namespace osu.Game.Overlays.Comments
{ {
@ -137,12 +138,13 @@ namespace osu.Game.Overlays.Comments
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0), Spacing = new Vector2(10, 0),
Children = new Drawable[] Children = new[]
{ {
username = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold)) username = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold))
{ {
AutoSizeAxes = Axes.Both AutoSizeAxes = Axes.Both
}, },
Comment.Pinned ? new PinnedCommentNotice() : Empty(),
new ParentUsername(Comment), new ParentUsername(Comment),
new OsuSpriteText new OsuSpriteText
{ {
@ -321,9 +323,7 @@ namespace osu.Game.Overlays.Comments
this.FadeTo(show.NewValue ? 1 : 0); this.FadeTo(show.NewValue ? 1 : 0);
}, true); }, true);
childrenExpanded.BindValueChanged(expanded => childCommentsVisibilityContainer.FadeTo(expanded.NewValue ? 1 : 0), true); childrenExpanded.BindValueChanged(expanded => childCommentsVisibilityContainer.FadeTo(expanded.NewValue ? 1 : 0), true);
updateButtonsState(); updateButtonsState();
base.LoadComplete(); base.LoadComplete();
} }
@ -392,6 +392,33 @@ namespace osu.Game.Overlays.Comments
}; };
} }
private class PinnedCommentNotice : FillFlowContainer
{
public PinnedCommentNotice()
{
AutoSizeAxes = Axes.Both;
Direction = FillDirection.Horizontal;
Spacing = new Vector2(2, 0);
Children = new Drawable[]
{
new SpriteIcon
{
Icon = FontAwesome.Solid.Thumbtack,
Size = new Vector2(14),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
new OsuSpriteText
{
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold),
Text = CommentsStrings.Pinned,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}
};
}
}
private class ParentUsername : FillFlowContainer, IHasTooltip private class ParentUsername : FillFlowContainer, IHasTooltip
{ {
public LocalisableString TooltipText => getParentMessage(); public LocalisableString TooltipText => getParentMessage();

View File

@ -0,0 +1,64 @@
// 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.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Mods
{
public class IncompatibleIcon : VisibilityContainer, IHasTooltip
{
private Circle circle;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Size = new Vector2(20);
State.Value = Visibility.Hidden;
Alpha = 0;
InternalChildren = new Drawable[]
{
circle = new Circle
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray4,
},
new SpriteIcon
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(0.6f),
Icon = FontAwesome.Solid.Slash,
Colour = Color4.White,
Shadow = true,
}
};
}
protected override void PopIn()
{
this.FadeIn(200, Easing.OutQuint);
circle.FlashColour(Color4.Red, 500, Easing.OutQuint);
this.ScaleTo(1.8f).Then().ScaleTo(1, 500, Easing.OutQuint);
}
protected override void PopOut()
{
this.FadeOut(200, Easing.OutQuint);
this.ScaleTo(0.8f, 200, Easing.In);
}
public LocalisableString TooltipText => "Incompatible with current selected mods";
}
}

View File

@ -11,24 +11,29 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Utils;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
/// <summary> /// <summary>
/// Represents a clickable button which can cycle through one of more mods. /// Represents a clickable button which can cycle through one of more mods.
/// </summary> /// </summary>
public class ModButton : ModButtonEmpty, IHasTooltip public class ModButton : ModButtonEmpty, IHasCustomTooltip
{ {
private ModIcon foregroundIcon; private ModIcon foregroundIcon;
private ModIcon backgroundIcon; private ModIcon backgroundIcon;
private readonly SpriteText text; private readonly SpriteText text;
private readonly Container<ModIcon> iconsContainer; private readonly Container<ModIcon> iconsContainer;
private readonly CompositeDrawable incompatibleIcon;
/// <summary> /// <summary>
/// Fired when the selection changes. /// Fired when the selection changes.
@ -43,6 +48,9 @@ namespace osu.Game.Overlays.Mods
// A selected index of -1 means not selected. // A selected index of -1 means not selected.
private int selectedIndex = -1; private int selectedIndex = -1;
[Resolved]
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; }
/// <summary> /// <summary>
/// Change the selected mod index of this button. /// Change the selected mod index of this button.
/// </summary> /// </summary>
@ -237,6 +245,23 @@ namespace osu.Game.Overlays.Mods
foregroundIcon.Mod = mod; foregroundIcon.Mod = mod;
text.Text = mod.Name; text.Text = mod.Name;
Colour = mod.HasImplementation ? Color4.White : Color4.Gray; Colour = mod.HasImplementation ? Color4.White : Color4.Gray;
Scheduler.AddOnce(updateCompatibility);
}
private void updateCompatibility()
{
var m = SelectedMod ?? Mods.First();
bool isIncompatible = false;
if (selectedMods.Value.Count > 0 && !selectedMods.Value.Contains(m))
isIncompatible = !ModUtils.CheckCompatibleSet(selectedMods.Value.Append(m));
if (isIncompatible)
incompatibleIcon.Show();
else
incompatibleIcon.Hide();
} }
private void createIcons() private void createIcons()
@ -284,11 +309,20 @@ namespace osu.Game.Overlays.Mods
{ {
scaleContainer = new Container scaleContainer = new Container
{ {
Child = iconsContainer = new Container<ModIcon> Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.Both, iconsContainer = new Container<ModIcon>
Origin = Anchor.Centre, {
Anchor = Anchor.Centre, RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
},
incompatibleIcon = new IncompatibleIcon
{
Origin = Anchor.Centre,
Anchor = Anchor.BottomRight,
Position = new Vector2(-13),
}
}, },
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -305,8 +339,18 @@ namespace osu.Game.Overlays.Mods
}, },
new HoverSounds() new HoverSounds()
}; };
Mod = mod; Mod = mod;
} }
protected override void LoadComplete()
{
base.LoadComplete();
selectedMods.BindValueChanged(_ => Scheduler.AddOnce(updateCompatibility), true);
}
public ITooltip GetCustomTooltip() => new ModButtonTooltip();
public object TooltipContent => SelectedMod ?? Mods.FirstOrDefault();
} }
} }

View File

@ -0,0 +1,115 @@
// 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.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play.HUD;
using osuTK;
namespace osu.Game.Overlays.Mods
{
public class ModButtonTooltip : VisibilityContainer, ITooltip
{
private readonly OsuSpriteText descriptionText;
private readonly Box background;
private readonly OsuSpriteText incompatibleText;
private readonly Bindable<IReadOnlyList<Mod>> incompatibleMods = new Bindable<IReadOnlyList<Mod>>();
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; }
public ModButtonTooltip()
{
AutoSizeAxes = Axes.Both;
Masking = true;
CornerRadius = 5;
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Left = 10, Right = 10, Top = 5, Bottom = 5 },
Children = new Drawable[]
{
descriptionText = new OsuSpriteText
{
Font = OsuFont.GetFont(weight: FontWeight.Regular),
Margin = new MarginPadding { Bottom = 5 }
},
incompatibleText = new OsuSpriteText
{
Font = OsuFont.GetFont(weight: FontWeight.Regular),
Text = "Incompatible with:"
},
new ModDisplay
{
Current = incompatibleMods,
ExpansionMode = ExpansionMode.AlwaysExpanded,
Scale = new Vector2(0.7f)
}
}
},
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
background.Colour = colours.Gray3;
descriptionText.Colour = colours.BlueLighter;
incompatibleText.Colour = colours.BlueLight;
}
protected override void PopIn() => this.FadeIn(200, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(200, Easing.OutQuint);
private Mod lastMod;
public bool SetContent(object content)
{
if (!(content is Mod mod))
return false;
if (mod.Equals(lastMod)) return true;
lastMod = mod;
descriptionText.Text = mod.Description;
var incompatibleTypes = mod.IncompatibleMods;
var allMods = ruleset.Value.CreateInstance().GetAllMods();
incompatibleMods.Value = allMods.Where(m => m.GetType() != mod.GetType() && incompatibleTypes.Any(t => t.IsInstanceOfType(m))).ToList();
if (!incompatibleMods.Value.Any())
{
incompatibleText.Text = "Compatible with all mods";
return true;
}
incompatibleText.Text = "Incompatible with:";
return true;
}
public void Move(Vector2 pos) => Position = pos;
}
}

View File

@ -74,6 +74,7 @@ namespace osu.Game.Overlays.Mods
protected readonly ModSettingsContainer ModSettingsContainer; protected readonly ModSettingsContainer ModSettingsContainer;
[Cached]
public readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>()); public readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
private Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods; private Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods;

View File

@ -10,20 +10,18 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
{ {
public class MatchChatDisplay : StandAloneChatDisplay public class MatchChatDisplay : StandAloneChatDisplay
{ {
[Resolved(typeof(Room), nameof(Room.RoomID))] private readonly IBindable<int> channelId = new Bindable<int>();
private Bindable<long?> roomId { get; set; }
[Resolved(typeof(Room), nameof(Room.ChannelId))]
private Bindable<int> channelId { get; set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private ChannelManager channelManager { get; set; } private ChannelManager channelManager { get; set; }
private readonly Room room;
private readonly bool leaveChannelOnDispose; private readonly bool leaveChannelOnDispose;
public MatchChatDisplay(bool leaveChannelOnDispose = true) public MatchChatDisplay(Room room, bool leaveChannelOnDispose = true)
: base(true) : base(true)
{ {
this.room = room;
this.leaveChannelOnDispose = leaveChannelOnDispose; this.leaveChannelOnDispose = leaveChannelOnDispose;
} }
@ -31,15 +29,17 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
{ {
base.LoadComplete(); base.LoadComplete();
// Required for the time being since this component is created prior to the room being joined.
channelId.BindTo(room.ChannelId);
channelId.BindValueChanged(_ => updateChannel(), true); channelId.BindValueChanged(_ => updateChannel(), true);
} }
private void updateChannel() private void updateChannel()
{ {
if (roomId.Value == null || channelId.Value == 0) if (room.RoomID.Value == null || channelId.Value == 0)
return; return;
Channel.Value = channelManager?.JoinChannel(new Channel { Id = channelId.Value, Type = ChannelType.Multiplayer, Name = $"#lazermp_{roomId.Value}" }); Channel.Value = channelManager?.JoinChannel(new Channel { Id = channelId.Value, Type = ChannelType.Multiplayer, Name = $"#lazermp_{room.RoomID.Value}" });
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -29,8 +30,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
public override bool PropagateNonPositionalInputSubTree => true; public override bool PropagateNonPositionalInputSubTree => true;
public GameplayChatDisplay() public GameplayChatDisplay(Room room)
: base(leaveChannelOnDispose: false) : base(room, leaveChannelOnDispose: false)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;

View File

@ -173,7 +173,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}, },
}, },
new Drawable[] { new OverlinedHeader("Chat") { Margin = new MarginPadding { Vertical = 5 }, }, }, new Drawable[] { new OverlinedHeader("Chat") { Margin = new MarginPadding { Vertical = 5 }, }, },
new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } } new Drawable[] { new MatchChatDisplay(Room) { RelativeSizeAxes = Axes.Both } }
}, },
RowDimensions = new[] RowDimensions = new[]
{ {
@ -395,7 +395,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return new MultiSpectatorScreen(users.Take(PlayerGrid.MAX_PLAYERS).ToArray()); return new MultiSpectatorScreen(users.Take(PlayerGrid.MAX_PLAYERS).ToArray());
default: default:
return new PlayerLoader(() => new MultiplayerPlayer(SelectedItem.Value, users)); return new PlayerLoader(() => new MultiplayerPlayer(Room, SelectedItem.Value, users));
} }
} }

View File

@ -48,10 +48,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
/// <summary> /// <summary>
/// Construct a multiplayer player. /// Construct a multiplayer player.
/// </summary> /// </summary>
/// <param name="room">The room.</param>
/// <param name="playlistItem">The playlist item to be played.</param> /// <param name="playlistItem">The playlist item to be played.</param>
/// <param name="users">The users which are participating in this game.</param> /// <param name="users">The users which are participating in this game.</param>
public MultiplayerPlayer(PlaylistItem playlistItem, MultiplayerRoomUser[] users) public MultiplayerPlayer(Room room, PlaylistItem playlistItem, MultiplayerRoomUser[] users)
: base(playlistItem, new PlayerConfiguration : base(room, playlistItem, new PlayerConfiguration
{ {
AllowPause = false, AllowPause = false,
AllowRestart = false, AllowRestart = false,
@ -95,7 +96,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
} }
}); });
LoadComponentAsync(new GameplayChatDisplay LoadComponentAsync(new GameplayChatDisplay(Room)
{ {
Expanded = { BindTarget = HUDOverlay.ShowHud }, Expanded = { BindTarget = HUDOverlay.ShowHud },
}, chat => leaderboardFlow.Insert(2, chat)); }, chat => leaderboardFlow.Insert(2, chat));
@ -189,10 +190,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override ResultsScreen CreateResults(ScoreInfo score) protected override ResultsScreen CreateResults(ScoreInfo score)
{ {
Debug.Assert(RoomId.Value != null); Debug.Assert(Room.RoomID.Value != null);
return leaderboard.TeamScores.Count == 2 return leaderboard.TeamScores.Count == 2
? new MultiplayerTeamResultsScreen(score, RoomId.Value.Value, PlaylistItem, leaderboard.TeamScores) ? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem, leaderboard.TeamScores)
: new MultiplayerResultsScreen(score, RoomId.Value.Value, PlaylistItem); : new MultiplayerResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem);
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -23,8 +23,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override UserActivity InitialActivity => new UserActivity.InPlaylistGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); protected override UserActivity InitialActivity => new UserActivity.InPlaylistGame(Beatmap.Value.BeatmapInfo, Ruleset.Value);
public PlaylistsPlayer(PlaylistItem playlistItem, PlayerConfiguration configuration = null) public PlaylistsPlayer(Room room, PlaylistItem playlistItem, PlayerConfiguration configuration = null)
: base(playlistItem, configuration) : base(room, playlistItem, configuration)
{ {
} }
@ -54,8 +54,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override ResultsScreen CreateResults(ScoreInfo score) protected override ResultsScreen CreateResults(ScoreInfo score)
{ {
Debug.Assert(RoomId.Value != null); Debug.Assert(Room.RoomID.Value != null);
return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true); return new PlaylistsResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem, true);
} }
protected override async Task PrepareScoreForResultsAsync(Score score) protected override async Task PrepareScoreForResultsAsync(Score score)

View File

@ -158,7 +158,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}, },
new Drawable[] { leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }, }, new Drawable[] { leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }, },
new Drawable[] { new OverlinedHeader("Chat"), }, new Drawable[] { new OverlinedHeader("Chat"), },
new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } } new Drawable[] { new MatchChatDisplay(Room) { RelativeSizeAxes = Axes.Both } }
}, },
RowDimensions = new[] RowDimensions = new[]
{ {
@ -199,7 +199,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
Logger.Log($"Polling adjusted (selection: {selectionPollingComponent.TimeBetweenPolls.Value})"); Logger.Log($"Polling adjusted (selection: {selectionPollingComponent.TimeBetweenPolls.Value})");
} }
protected override Screen CreateGameplayScreen() => new PlayerLoader(() => new PlaylistsPlayer(SelectedItem.Value) protected override Screen CreateGameplayScreen() => new PlayerLoader(() => new PlaylistsPlayer(Room, SelectedItem.Value)
{ {
Exited = () => leaderboard.RefreshScores() Exited = () => leaderboard.RefreshScores()
}); });

View File

@ -1,8 +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 osu.Framework.Allocation; using System.Diagnostics;
using osu.Framework.Bindables;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -14,25 +13,28 @@ namespace osu.Game.Screens.Play
/// </summary> /// </summary>
public abstract class RoomSubmittingPlayer : SubmittingPlayer public abstract class RoomSubmittingPlayer : SubmittingPlayer
{ {
[Resolved(typeof(Room), nameof(Room.RoomID))]
protected Bindable<long?> RoomId { get; private set; }
protected readonly PlaylistItem PlaylistItem; protected readonly PlaylistItem PlaylistItem;
protected readonly Room Room;
protected RoomSubmittingPlayer(PlaylistItem playlistItem, PlayerConfiguration configuration = null) protected RoomSubmittingPlayer(Room room, PlaylistItem playlistItem, PlayerConfiguration configuration = null)
: base(configuration) : base(configuration)
{ {
Room = room;
PlaylistItem = playlistItem; PlaylistItem = playlistItem;
} }
protected override APIRequest<APIScoreToken> CreateTokenRequest() protected override APIRequest<APIScoreToken> CreateTokenRequest()
{ {
if (!(RoomId.Value is long roomId)) if (!(Room.RoomID.Value is long roomId))
return null; return null;
return new CreateRoomScoreRequest(roomId, PlaylistItem.ID, Game.VersionHash); return new CreateRoomScoreRequest(roomId, PlaylistItem.ID, Game.VersionHash);
} }
protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token) => new SubmitRoomScoreRequest(token, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo); protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token)
{
Debug.Assert(Room.RoomID.Value != null);
return new SubmitRoomScoreRequest(token, Room.RoomID.Value.Value, PlaylistItem.ID, score.ScoreInfo);
}
} }
} }

View File

@ -44,9 +44,6 @@ namespace osu.Game.Screens.Select
[Resolved] [Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } private IBindable<RulesetInfo> ruleset { get; set; }
[Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
protected Container DisplayedContent { get; private set; } protected Container DisplayedContent { get; private set; }
protected WedgeInfoText Info { get; private set; } protected WedgeInfoText Info { get; private set; }
@ -71,7 +68,6 @@ namespace osu.Game.Screens.Select
private void load() private void load()
{ {
ruleset.BindValueChanged(_ => updateDisplay()); ruleset.BindValueChanged(_ => updateDisplay());
mods.BindValueChanged(_ => updateDisplay());
} }
private const double animation_duration = 800; private const double animation_duration = 800;
@ -138,7 +134,7 @@ namespace osu.Game.Screens.Select
Children = new Drawable[] Children = new Drawable[]
{ {
new BeatmapInfoWedgeBackground(beatmap), new BeatmapInfoWedgeBackground(beatmap),
Info = new WedgeInfoText(beatmap, ruleset.Value, mods.Value), Info = new WedgeInfoText(beatmap, ruleset.Value),
} }
}, loaded => }, loaded =>
{ {
@ -169,15 +165,16 @@ namespace osu.Game.Screens.Select
private readonly WorkingBeatmap beatmap; private readonly WorkingBeatmap beatmap;
private readonly RulesetInfo ruleset; private readonly RulesetInfo ruleset;
private readonly IReadOnlyList<Mod> mods;
[Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
private ModSettingChangeTracker settingChangeTracker; private ModSettingChangeTracker settingChangeTracker;
public WedgeInfoText(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList<Mod> mods) public WedgeInfoText(WorkingBeatmap beatmap, RulesetInfo userRuleset)
{ {
this.beatmap = beatmap; this.beatmap = beatmap;
ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset; ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset;
this.mods = mods;
} }
private CancellationTokenSource cancellationSource; private CancellationTokenSource cancellationSource;
@ -363,10 +360,15 @@ namespace osu.Game.Screens.Select
} }
}; };
settingChangeTracker = new ModSettingChangeTracker(mods); mods.BindValueChanged(m =>
settingChangeTracker.SettingChanged += _ => refreshBPMLabel(); {
settingChangeTracker?.Dispose();
refreshBPMLabel(); refreshBPMLabel();
settingChangeTracker = new ModSettingChangeTracker(m.NewValue);
settingChangeTracker.SettingChanged += _ => refreshBPMLabel();
}, true);
} }
private InfoLabel[] getRulesetInfoLabels() private InfoLabel[] getRulesetInfoLabels()
@ -404,7 +406,7 @@ namespace osu.Game.Screens.Select
// this doesn't consider mods which apply variable rates, yet. // this doesn't consider mods which apply variable rates, yet.
double rate = 1; double rate = 1;
foreach (var mod in mods.OfType<IApplicableToRate>()) foreach (var mod in mods.Value.OfType<IApplicableToRate>())
rate = mod.ApplyToRate(0, rate); rate = mod.ApplyToRate(0, rate);
double bpmMax = b.ControlPointInfo.BPMMaximum * rate; double bpmMax = b.ControlPointInfo.BPMMaximum * rate;

View File

@ -136,18 +136,19 @@ namespace osu.Game.Skinning
protected override string ComputeHash(SkinInfo item, ArchiveReader reader = null) protected override string ComputeHash(SkinInfo item, ArchiveReader reader = null)
{ {
// we need to populate early to create a hash based off skin.ini contents var instance = GetSkin(item);
if (item.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true)
populateMetadata(item, GetSkin(item));
if (item.Creator != null && item.Creator != unknown_creator_string) // in the case the skin has a skin.ini file, we are going to create a hash based on that.
// we don't want to do this in the case we don't have a skin.ini, as it would match only on the filename portion,
// causing potentially unique skin imports to be considered as a duplicate.
if (!string.IsNullOrEmpty(instance.Configuration.SkinInfo.Name))
{ {
// this is the optimal way to hash legacy skins, but will need to be reconsidered when we move forward with skin implementation. // we need to populate early to create a hash based off skin.ini contents
// likely, the skin should expose a real version (ie. the version of the skin, not the skin.ini version it's targeting). populateMetadata(item, instance, reader?.Name);
return item.ToString().ComputeSHA2Hash(); return item.ToString().ComputeSHA2Hash();
} }
// if there was no creator, the ToString above would give the filename, which alone isn't really enough to base any decisions on.
return base.ComputeHash(item, reader); return base.ComputeHash(item, reader);
} }
@ -157,13 +158,12 @@ namespace osu.Game.Skinning
model.InstantiationInfo ??= instance.GetType().GetInvariantInstantiationInfo(); model.InstantiationInfo ??= instance.GetType().GetInvariantInstantiationInfo();
if (model.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true) populateMetadata(model, instance, archive?.Name);
populateMetadata(model, instance);
return Task.CompletedTask; return Task.CompletedTask;
} }
private void populateMetadata(SkinInfo item, Skin instance) private void populateMetadata(SkinInfo item, Skin instance, string archiveName)
{ {
if (!string.IsNullOrEmpty(instance.Configuration.SkinInfo.Name)) if (!string.IsNullOrEmpty(instance.Configuration.SkinInfo.Name))
{ {
@ -175,6 +175,13 @@ namespace osu.Game.Skinning
item.Name = item.Name.Replace(".osk", "", StringComparison.OrdinalIgnoreCase); item.Name = item.Name.Replace(".osk", "", StringComparison.OrdinalIgnoreCase);
item.Creator ??= unknown_creator_string; item.Creator ??= unknown_creator_string;
} }
// generally when importing from a folder, the ".osk" extension will not be present.
// if we ever need a more reliable method of determining this, the type of `ArchiveReader` can be checked.
bool isArchiveImport = archiveName?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true;
if (archiveName != null && !isArchiveImport && archiveName != item.Name)
item.Name = $"{item.Name} [{archiveName}]";
} }
/// <summary> /// <summary>

View File

@ -18,6 +18,8 @@ namespace osu.Game.Skinning
private readonly BindableList<ISkinnableDrawable> components = new BindableList<ISkinnableDrawable>(); private readonly BindableList<ISkinnableDrawable> components = new BindableList<ISkinnableDrawable>();
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; // ensure that components are loaded even if the target container is hidden (ie. due to user toggle).
public bool ComponentsLoaded { get; private set; } public bool ComponentsLoaded { get; private set; }
public SkinnableTargetContainer(SkinnableTarget target) public SkinnableTargetContainer(SkinnableTarget target)