mirror of
https://github.com/ppy/osu.git
synced 2025-03-17 22:17:25 +08:00
Merge branch 'ppy:master' into colour-rework
This commit is contained in:
commit
2e004bf551
@ -21,7 +21,7 @@
|
||||
]
|
||||
},
|
||||
"ppy.localisationanalyser.tools": {
|
||||
"version": "2022.417.0",
|
||||
"version": "2022.607.0",
|
||||
"commands": [
|
||||
"localisation"
|
||||
]
|
||||
|
@ -30,6 +30,9 @@ namespace osu.Desktop
|
||||
|
||||
private IBindable<APIUser> user;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
private readonly IBindable<UserStatus> status = new Bindable<UserStatus>();
|
||||
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
|
||||
|
||||
@ -41,7 +44,7 @@ namespace osu.Desktop
|
||||
};
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IAPIProvider provider, OsuConfigManager config)
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
client = new DiscordRpcClient(client_id)
|
||||
{
|
||||
@ -57,7 +60,8 @@ namespace osu.Desktop
|
||||
|
||||
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
|
||||
|
||||
(user = provider.LocalUser.GetBoundCopy()).BindValueChanged(u =>
|
||||
user = api.LocalUser.GetBoundCopy();
|
||||
user.BindValueChanged(u =>
|
||||
{
|
||||
status.UnbindBindings();
|
||||
status.BindTo(u.NewValue.Status);
|
||||
@ -106,7 +110,12 @@ namespace osu.Desktop
|
||||
if (privacyMode.Value == DiscordRichPresenceMode.Limited)
|
||||
presence.Assets.LargeImageText = string.Empty;
|
||||
else
|
||||
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
|
||||
{
|
||||
if (user.Value.RulesetsStatistics != null && user.Value.RulesetsStatistics.TryGetValue(ruleset.Value.ShortName, out UserStatistics statistics))
|
||||
presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty);
|
||||
else
|
||||
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
|
||||
}
|
||||
|
||||
// update ruleset
|
||||
presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom";
|
||||
|
@ -79,7 +79,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
Result = { BindTarget = SpinsPerMinute },
|
||||
},
|
||||
ticks = new Container<DrawableSpinnerTick>(),
|
||||
ticks = new Container<DrawableSpinnerTick>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new AspectContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
|
@ -1,6 +1,8 @@
|
||||
// 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.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public class DrawableSpinnerTick : DrawableOsuHitObject
|
||||
@ -10,13 +12,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
protected DrawableSpinner DrawableSpinner => (DrawableSpinner)ParentHitObject;
|
||||
|
||||
public DrawableSpinnerTick()
|
||||
: base(null)
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public DrawableSpinnerTick(SpinnerTick spinnerTick)
|
||||
: base(spinnerTick)
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
protected override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
|
||||
|
@ -69,8 +69,8 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
|
||||
|
||||
AddNested(i < SpinsRequired
|
||||
? new SpinnerTick { StartTime = startTime, Position = Position }
|
||||
: new SpinnerBonusTick { StartTime = startTime, Position = Position });
|
||||
? new SpinnerTick { StartTime = startTime }
|
||||
: new SpinnerBonusTick { StartTime = startTime });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@ -321,12 +322,14 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
private class ProxyContainer : LifetimeManagementContainer
|
||||
{
|
||||
public new MarginPadding Padding
|
||||
{
|
||||
set => base.Padding = value;
|
||||
}
|
||||
|
||||
public void Add(Drawable proxy) => AddInternal(proxy);
|
||||
|
||||
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds)
|
||||
{
|
||||
// DrawableHitObject disables masking.
|
||||
// Hitobject content is proxied and unproxied based on hit status and the IsMaskedAway value could get stuck because of this.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Tests.Chat
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
var container = new ChannelManagerContainer();
|
||||
var container = new ChannelManagerContainer(API);
|
||||
Child = container;
|
||||
channelManager = container.ChannelManager;
|
||||
});
|
||||
@ -145,11 +145,11 @@ namespace osu.Game.Tests.Chat
|
||||
private class ChannelManagerContainer : CompositeDrawable
|
||||
{
|
||||
[Cached]
|
||||
public ChannelManager ChannelManager { get; } = new ChannelManager();
|
||||
public ChannelManager ChannelManager { get; }
|
||||
|
||||
public ChannelManagerContainer()
|
||||
public ChannelManagerContainer(IAPIProvider apiProvider)
|
||||
{
|
||||
InternalChild = ChannelManager;
|
||||
InternalChild = ChannelManager = new ChannelManager(apiProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,12 +59,14 @@ namespace osu.Game.Tests.Gameplay
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TestJudgement(HitResult.Great)) { Type = HitResult.Great });
|
||||
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
|
||||
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
||||
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1));
|
||||
|
||||
// No header shouldn't cause any change
|
||||
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame());
|
||||
|
||||
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
|
||||
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
||||
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1));
|
||||
|
||||
// Reset with a miss instead.
|
||||
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
|
||||
@ -74,6 +76,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
|
||||
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
|
||||
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
||||
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
|
||||
|
||||
// Reset with no judged hit.
|
||||
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
|
||||
@ -83,6 +86,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
|
||||
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
|
||||
Assert.That(scoreProcessor.JudgedHits, Is.Zero);
|
||||
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
private class TestJudgement : Judgement
|
||||
|
@ -59,11 +59,13 @@ namespace osu.Game.Tests.Skins
|
||||
AddAssert("Check float parse lookup", () => requester.GetConfig<string, float>("FloatTest")?.Value == 1.1f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBoolLookup()
|
||||
[TestCase("0", false)]
|
||||
[TestCase("1", true)]
|
||||
[TestCase("2", true)] // https://github.com/ppy/osu/issues/18579
|
||||
public void TestBoolLookup(string originalValue, bool expectedParsedValue)
|
||||
{
|
||||
AddStep("Add config values", () => userSource.Configuration.ConfigDictionary["BoolTest"] = "1");
|
||||
AddAssert("Check bool parse lookup", () => requester.GetConfig<string, bool>("BoolTest")?.Value == true);
|
||||
AddStep("Add config values", () => userSource.Configuration.ConfigDictionary["BoolTest"] = originalValue);
|
||||
AddAssert("Check bool parse lookup", () => requester.GetConfig<string, bool>("BoolTest")?.Value == expectedParsedValue);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -18,7 +18,6 @@ using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Replays.Legacy;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
@ -27,7 +26,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public abstract class MultiplayerGameplayLeaderboardTestScene : OsuTestScene
|
||||
{
|
||||
private const int total_users = 16;
|
||||
protected const int TOTAL_USERS = 16;
|
||||
|
||||
protected readonly BindableList<MultiplayerRoomUser> MultiplayerUsers = new BindableList<MultiplayerRoomUser>();
|
||||
|
||||
@ -35,9 +34,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
protected virtual MultiplayerRoomUser CreateUser(int userId) => new MultiplayerRoomUser(userId);
|
||||
|
||||
protected abstract MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor);
|
||||
protected abstract MultiplayerGameplayLeaderboard CreateLeaderboard();
|
||||
|
||||
private readonly BindableList<int> multiplayerUserIds = new BindableList<int>();
|
||||
private readonly BindableDictionary<int, SpectatorState> watchedUserStates = new BindableDictionary<int, SpectatorState>();
|
||||
|
||||
private OsuConfigManager config;
|
||||
|
||||
@ -81,6 +81,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
multiplayerClient.SetupGet(c => c.CurrentMatchPlayingUserIds)
|
||||
.Returns(() => multiplayerUserIds);
|
||||
|
||||
spectatorClient.SetupGet(c => c.WatchedUserStates)
|
||||
.Returns(() => watchedUserStates);
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
@ -100,8 +103,26 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("populate users", () =>
|
||||
{
|
||||
MultiplayerUsers.Clear();
|
||||
for (int i = 0; i < total_users; i++)
|
||||
MultiplayerUsers.Add(CreateUser(i));
|
||||
|
||||
for (int i = 0; i < TOTAL_USERS; i++)
|
||||
{
|
||||
var user = CreateUser(i);
|
||||
|
||||
MultiplayerUsers.Add(user);
|
||||
|
||||
watchedUserStates[i] = new SpectatorState
|
||||
{
|
||||
BeatmapID = 0,
|
||||
RulesetID = 0,
|
||||
Mods = user.Mods,
|
||||
MaximumScoringValues = new ScoringValues
|
||||
{
|
||||
BaseScore = 10000,
|
||||
MaxCombo = 1000,
|
||||
CountBasicHitObjects = 1000
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("create leaderboard", () =>
|
||||
@ -109,13 +130,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Leaderboard?.Expire();
|
||||
|
||||
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
|
||||
var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
|
||||
OsuScoreProcessor scoreProcessor = new OsuScoreProcessor();
|
||||
scoreProcessor.ApplyBeatmap(playableBeatmap);
|
||||
|
||||
Child = scoreProcessor;
|
||||
|
||||
LoadComponentAsync(Leaderboard = CreateLeaderboard(scoreProcessor), Add);
|
||||
LoadComponentAsync(Leaderboard = CreateLeaderboard(), Add);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for load", () => Leaderboard.IsLoaded);
|
||||
|
@ -4,11 +4,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("reset", () =>
|
||||
{
|
||||
Clear();
|
||||
leaderboard?.RemoveAndDisposeImmediately();
|
||||
|
||||
clocks = new Dictionary<int, ManualClock>
|
||||
{
|
||||
@ -32,21 +32,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{ PLAYER_2_ID, new ManualClock() }
|
||||
};
|
||||
|
||||
foreach ((int userId, var _) in clocks)
|
||||
foreach ((int userId, _) in clocks)
|
||||
{
|
||||
SpectatorClient.SendStartPlay(userId, 0);
|
||||
OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = userId });
|
||||
OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = userId }, true);
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("create leaderboard", () =>
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
|
||||
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
|
||||
var scoreProcessor = new OsuScoreProcessor();
|
||||
scoreProcessor.ApplyBeatmap(playable);
|
||||
|
||||
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(Ruleset.Value, scoreProcessor, clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray())
|
||||
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray())
|
||||
{
|
||||
Expanded = { Value = true }
|
||||
}, Add);
|
||||
|
@ -1,22 +1,58 @@
|
||||
// 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.Graphics;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerGameplayLeaderboardTestScene
|
||||
{
|
||||
protected override MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor)
|
||||
protected override MultiplayerRoomUser CreateUser(int userId)
|
||||
{
|
||||
return new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, MultiplayerUsers.ToArray())
|
||||
var user = base.CreateUser(userId);
|
||||
|
||||
if (userId == TOTAL_USERS - 1)
|
||||
user.Mods = new[] { new APIMod(new OsuModNoFail()) };
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
protected override MultiplayerGameplayLeaderboard CreateLeaderboard()
|
||||
{
|
||||
return new TestLeaderboard(MultiplayerUsers.ToArray())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPerUserMods()
|
||||
{
|
||||
AddStep("first user has no mods", () => Assert.That(((TestLeaderboard)Leaderboard).UserMods[0], Is.Empty));
|
||||
AddStep("last user has NF mod", () =>
|
||||
{
|
||||
Assert.That(((TestLeaderboard)Leaderboard).UserMods[TOTAL_USERS - 1], Has.One.Items);
|
||||
Assert.That(((TestLeaderboard)Leaderboard).UserMods[TOTAL_USERS - 1].Single(), Is.TypeOf<OsuModNoFail>());
|
||||
});
|
||||
}
|
||||
|
||||
private class TestLeaderboard : MultiplayerGameplayLeaderboard
|
||||
{
|
||||
public Dictionary<int, IReadOnlyList<Mod>> UserMods => UserScores.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ScoreProcessor.Mods);
|
||||
|
||||
public TestLeaderboard(MultiplayerRoomUser[] users)
|
||||
: base(users)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
@ -25,8 +24,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
return user;
|
||||
}
|
||||
|
||||
protected override MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor) =>
|
||||
new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, MultiplayerUsers.ToArray())
|
||||
protected override MultiplayerGameplayLeaderboard CreateLeaderboard() =>
|
||||
new MultiplayerGameplayLeaderboard(MultiplayerUsers.ToArray())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
|
@ -609,8 +609,6 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
public ModSelectOverlay ModSelectOverlay => ModSelect;
|
||||
|
||||
public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions;
|
||||
|
||||
protected override bool DisplayStableImportPrompt => false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
linkColour = colours.Blue;
|
||||
|
||||
var chatManager = new ChannelManager();
|
||||
var chatManager = new ChannelManager(API);
|
||||
BindableList<Channel> availableChannels = (BindableList<Channel>)chatManager.AvailableChannels;
|
||||
availableChannels.Add(new Channel { Name = "#english" });
|
||||
availableChannels.Add(new Channel { Name = "#japanese" });
|
||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies = new (Type, object)[]
|
||||
{
|
||||
(typeof(ChannelManager), channelManager = new ChannelManager()),
|
||||
(typeof(ChannelManager), channelManager = new ChannelManager(API)),
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -572,15 +572,15 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
public SlowLoadingDrawableChannel GetSlowLoadingChannel(Channel channel) => DrawableChannels.OfType<SlowLoadingDrawableChannel>().Single(c => c.Channel == channel);
|
||||
|
||||
protected override ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel)
|
||||
protected override DrawableChannel CreateDrawableChannel(Channel newChannel)
|
||||
{
|
||||
return SlowLoading
|
||||
? new SlowLoadingDrawableChannel(newChannel)
|
||||
: new ChatOverlayDrawableChannel(newChannel);
|
||||
: new DrawableChannel(newChannel);
|
||||
}
|
||||
}
|
||||
|
||||
private class SlowLoadingDrawableChannel : ChatOverlayDrawableChannel
|
||||
private class SlowLoadingDrawableChannel : DrawableChannel
|
||||
{
|
||||
public readonly ManualResetEventSlim LoadEvent = new ManualResetEventSlim();
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@ -43,7 +44,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
Child = testContainer = new TestContainer(new[] { publicChannel, privateMessageChannel })
|
||||
Child = testContainer = new TestContainer(API, new[] { publicChannel, privateMessageChannel })
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
@ -178,6 +179,36 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddAssert("1 notification fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that <see cref="MessageNotifier"/> handles channels which have not been or could not be resolved (i.e. <see cref="Channel.Id"/> = 0).
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestSendInUnresolvedChannel()
|
||||
{
|
||||
int i = 1;
|
||||
Channel unresolved = null;
|
||||
|
||||
AddRepeatStep("join unresolved channels", () => testContainer.ChannelManager.JoinChannel(unresolved = new Channel(new APIUser
|
||||
{
|
||||
Id = 100 + i,
|
||||
Username = $"Foreign #{i++}",
|
||||
})), 5);
|
||||
|
||||
AddStep("send message in unresolved channel", () =>
|
||||
{
|
||||
Debug.Assert(unresolved.Id == 0);
|
||||
|
||||
unresolved.AddLocalEcho(new LocalEchoMessage
|
||||
{
|
||||
Sender = API.LocalUser.Value,
|
||||
ChannelId = unresolved.Id,
|
||||
Content = "Some message",
|
||||
});
|
||||
});
|
||||
|
||||
AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0);
|
||||
}
|
||||
|
||||
private void receiveMessage(APIUser sender, Channel channel, string content) => channel.AddNewMessages(createMessage(sender, channel, content));
|
||||
|
||||
private Message createMessage(APIUser sender, Channel channel, string content) => new Message(messageIdCounter++)
|
||||
@ -198,7 +229,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
private class TestContainer : Container
|
||||
{
|
||||
[Cached]
|
||||
public ChannelManager ChannelManager { get; } = new ChannelManager();
|
||||
public ChannelManager ChannelManager { get; }
|
||||
|
||||
[Cached(typeof(INotificationOverlay))]
|
||||
public NotificationOverlay NotificationOverlay { get; } = new NotificationOverlay
|
||||
@ -214,9 +245,10 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
private readonly Channel[] channels;
|
||||
|
||||
public TestContainer(Channel[] channels)
|
||||
public TestContainer(IAPIProvider api, Channel[] channels)
|
||||
{
|
||||
this.channels = channels;
|
||||
ChannelManager = new ChannelManager(api);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -11,6 +11,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.Chat;
|
||||
using osuTK.Input;
|
||||
@ -44,17 +45,22 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Id = 5,
|
||||
};
|
||||
|
||||
[Cached]
|
||||
private ChannelManager channelManager = new ChannelManager();
|
||||
private ChannelManager channelManager;
|
||||
|
||||
private TestStandAloneChatDisplay chatDisplay;
|
||||
private int messageIdSequence;
|
||||
|
||||
private Channel testChannel;
|
||||
|
||||
public TestSceneStandAloneChatDisplay()
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
Add(channelManager);
|
||||
Add(channelManager = new ChannelManager(parent.Get<IAPIProvider>()));
|
||||
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
dependencies.Cache(channelManager);
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
@ -128,11 +134,11 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
AddAssert("Ensure no adjacent day separators", () =>
|
||||
{
|
||||
var indices = chatDisplay.FillFlow.OfType<DrawableChannel.DaySeparator>().Select(ds => chatDisplay.FillFlow.IndexOf(ds));
|
||||
var indices = chatDisplay.FillFlow.OfType<DaySeparator>().Select(ds => chatDisplay.FillFlow.IndexOf(ds));
|
||||
|
||||
foreach (int i in indices)
|
||||
{
|
||||
if (i < chatDisplay.FillFlow.Count && chatDisplay.FillFlow[i + 1] is DrawableChannel.DaySeparator)
|
||||
if (i < chatDisplay.FillFlow.Count && chatDisplay.FillFlow[i + 1] is DaySeparator)
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -96,6 +96,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
beatmap.Metadata.Author = author;
|
||||
beatmap.Metadata.Title = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap title";
|
||||
beatmap.Metadata.Artist = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap artist";
|
||||
beatmap.DifficultyName = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong difficulty name";
|
||||
|
||||
return beatmap;
|
||||
}
|
||||
|
@ -5,12 +5,11 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
@ -23,38 +22,28 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
public class TestSceneBeatmapRecommendations : OsuGameTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private IRulesetStore rulesetStore { get; set; }
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
AddStep("register request handling", () =>
|
||||
{
|
||||
((DummyAPIAccess)API).HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetUserRequest userRequest:
|
||||
userRequest.TriggerSuccess(getUser(userRequest.Ruleset.OnlineID));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
});
|
||||
|
||||
base.SetUpSteps();
|
||||
|
||||
APIUser getUser(int? rulesetID)
|
||||
AddStep("populate ruleset statistics", () =>
|
||||
{
|
||||
return new APIUser
|
||||
Dictionary<string, UserStatistics> rulesetStatistics = new Dictionary<string, UserStatistics>();
|
||||
|
||||
rulesetStore.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo =>
|
||||
{
|
||||
Username = @"Dummy",
|
||||
Id = 1001,
|
||||
Statistics = new UserStatistics
|
||||
rulesetStatistics[rulesetInfo.ShortName] = new UserStatistics
|
||||
{
|
||||
PP = getNecessaryPP(rulesetID)
|
||||
}
|
||||
};
|
||||
}
|
||||
PP = getNecessaryPP(rulesetInfo.OnlineID)
|
||||
};
|
||||
});
|
||||
|
||||
API.LocalUser.Value.RulesetsStatistics = rulesetStatistics;
|
||||
});
|
||||
|
||||
decimal getNecessaryPP(int? rulesetID)
|
||||
{
|
||||
|
@ -18,6 +18,7 @@ using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets;
|
||||
@ -80,6 +81,37 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddStep("delete all beatmaps", () => manager?.Delete());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceholderBeatmapPresence()
|
||||
{
|
||||
createSongSelect();
|
||||
|
||||
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
|
||||
|
||||
addRulesetImportStep(0);
|
||||
AddUntilStep("wait for placeholder hidden", () => getPlaceholder()?.State.Value == Visibility.Hidden);
|
||||
|
||||
AddStep("delete all beatmaps", () => manager?.Delete());
|
||||
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceholderConvertSetting()
|
||||
{
|
||||
changeRuleset(2);
|
||||
addRulesetImportStep(0);
|
||||
AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
|
||||
|
||||
createSongSelect();
|
||||
|
||||
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
|
||||
|
||||
AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType<DrawableLinkCompiler>().First().TriggerClick());
|
||||
|
||||
AddUntilStep("convert setting changed", () => config.Get<bool>(OsuSetting.ShowConvertedBeatmaps));
|
||||
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleFilterOnEnter()
|
||||
{
|
||||
@ -941,6 +973,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.IndexOf(info);
|
||||
|
||||
private NoResultsPlaceholder getPlaceholder() => songSelect.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault();
|
||||
|
||||
private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmapInfo);
|
||||
|
||||
private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, FilterableDifficultyIcon icon)
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays.Chat;
|
||||
using osu.Game.Tournament.IPC;
|
||||
@ -29,7 +30,7 @@ namespace osu.Game.Tournament.Components
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(MatchIPCInfo ipc)
|
||||
private void load(MatchIPCInfo ipc, IAPIProvider api)
|
||||
{
|
||||
if (ipc != null)
|
||||
{
|
||||
@ -45,7 +46,7 @@ namespace osu.Game.Tournament.Components
|
||||
|
||||
if (manager == null)
|
||||
{
|
||||
AddInternal(manager = new ChannelManager { HighPollRate = { Value = true } });
|
||||
AddInternal(manager = new ChannelManager(api) { HighPollRate = { Value = true } });
|
||||
Channel.BindTo(manager.CurrentChannel);
|
||||
}
|
||||
|
||||
|
@ -319,6 +319,15 @@ namespace osu.Game.Beatmaps
|
||||
});
|
||||
}
|
||||
|
||||
public void DeleteAllVideos()
|
||||
{
|
||||
realm.Write(r =>
|
||||
{
|
||||
var items = r.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected);
|
||||
beatmapModelManager.DeleteVideos(items.ToList());
|
||||
});
|
||||
}
|
||||
|
||||
public void UndeleteAll()
|
||||
{
|
||||
realm.Run(r => beatmapModelManager.Undelete(r.All<BeatmapSetInfo>().Where(s => s.DeletePending).ToList()));
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Stores;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@ -33,6 +34,8 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
protected override string[] HashableFileTypes => new[] { ".osu" };
|
||||
|
||||
public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" };
|
||||
|
||||
public BeatmapModelManager(RealmAccess realm, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null)
|
||||
: base(realm, storage, onlineLookupQueue)
|
||||
{
|
||||
@ -114,5 +117,50 @@ namespace osu.Game.Beatmaps
|
||||
item.CopyChangesToRealm(existing);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete videos from a list of beatmaps.
|
||||
/// This will post notifications tracking progress.
|
||||
/// </summary>
|
||||
public void DeleteVideos(List<BeatmapSetInfo> items, bool silent = false)
|
||||
{
|
||||
if (items.Count == 0) return;
|
||||
|
||||
var notification = new ProgressNotification
|
||||
{
|
||||
Progress = 0,
|
||||
Text = $"Preparing to delete all {HumanisedModelName} videos...",
|
||||
CompletionText = "No videos found to delete!",
|
||||
State = ProgressNotificationState.Active,
|
||||
};
|
||||
|
||||
if (!silent)
|
||||
PostNotification?.Invoke(notification);
|
||||
|
||||
int i = 0;
|
||||
int deleted = 0;
|
||||
|
||||
foreach (var b in items)
|
||||
{
|
||||
if (notification.State == ProgressNotificationState.Cancelled)
|
||||
// user requested abort
|
||||
return;
|
||||
|
||||
var video = b.Files.FirstOrDefault(f => VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.Ordinal)));
|
||||
|
||||
if (video != null)
|
||||
{
|
||||
DeleteFile(b, video);
|
||||
deleted++;
|
||||
notification.CompletionText = $"Deleted {deleted} {HumanisedModelName} video(s)!";
|
||||
}
|
||||
|
||||
notification.Text = $"Deleting videos from {HumanisedModelName}s ({deleted} deleted)";
|
||||
|
||||
notification.Progress = (float)++i / items.Count;
|
||||
}
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,11 +7,8 @@ using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
@ -25,26 +22,15 @@ namespace osu.Game.Beatmaps
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IRulesetStore rulesets { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Bindable<RulesetInfo> ruleset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user for which the last requests were run.
|
||||
/// </summary>
|
||||
private int? requestedUserId;
|
||||
|
||||
private readonly Dictionary<IRulesetInfo, double> recommendedDifficultyMapping = new Dictionary<IRulesetInfo, double>();
|
||||
|
||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||
private readonly Dictionary<string, double> recommendedDifficultyMapping = new Dictionary<string, double>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
apiState.BindTo(api.State);
|
||||
apiState.BindValueChanged(onlineStateChanged, true);
|
||||
api.LocalUser.BindValueChanged(_ => populateValues(), true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -58,12 +44,12 @@ namespace osu.Game.Beatmaps
|
||||
[CanBeNull]
|
||||
public BeatmapInfo GetRecommendedBeatmap(IEnumerable<BeatmapInfo> beatmaps)
|
||||
{
|
||||
foreach (var r in orderedRulesets)
|
||||
foreach (string r in orderedRulesets)
|
||||
{
|
||||
if (!recommendedDifficultyMapping.TryGetValue(r, out double recommendation))
|
||||
continue;
|
||||
|
||||
BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b =>
|
||||
BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.ShortName.Equals(r)).OrderBy(b =>
|
||||
{
|
||||
double difference = b.StarRating - recommendation;
|
||||
return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder
|
||||
@ -76,55 +62,35 @@ namespace osu.Game.Beatmaps
|
||||
return null;
|
||||
}
|
||||
|
||||
private void fetchRecommendedValues()
|
||||
private void populateValues()
|
||||
{
|
||||
if (recommendedDifficultyMapping.Count > 0 && api.LocalUser.Value.Id == requestedUserId)
|
||||
if (api.LocalUser.Value.RulesetsStatistics == null)
|
||||
return;
|
||||
|
||||
requestedUserId = api.LocalUser.Value.Id;
|
||||
|
||||
// only query API for built-in rulesets
|
||||
rulesets.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo =>
|
||||
foreach (var kvp in api.LocalUser.Value.RulesetsStatistics)
|
||||
{
|
||||
var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo);
|
||||
|
||||
req.Success += result =>
|
||||
{
|
||||
// algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
|
||||
recommendedDifficultyMapping[rulesetInfo] = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195;
|
||||
};
|
||||
|
||||
api.Queue(req);
|
||||
});
|
||||
// algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
|
||||
recommendedDifficultyMapping[kvp.Key] = Math.Pow((double)(kvp.Value.PP ?? 0), 0.4) * 0.195;
|
||||
}
|
||||
}
|
||||
|
||||
/// <returns>
|
||||
/// Rulesets ordered descending by their respective recommended difficulties.
|
||||
/// The currently selected ruleset will always be first.
|
||||
/// </returns>
|
||||
private IEnumerable<IRulesetInfo> orderedRulesets
|
||||
private IEnumerable<string> orderedRulesets
|
||||
{
|
||||
get
|
||||
{
|
||||
if (LoadState < LoadState.Ready || ruleset.Value == null)
|
||||
return Enumerable.Empty<RulesetInfo>();
|
||||
return Enumerable.Empty<string>();
|
||||
|
||||
return recommendedDifficultyMapping
|
||||
.OrderByDescending(pair => pair.Value)
|
||||
.Select(pair => pair.Key)
|
||||
.Where(r => !r.Equals(ruleset.Value))
|
||||
.Prepend(ruleset.Value);
|
||||
.Where(r => !r.Equals(ruleset.Value.ShortName))
|
||||
.Prepend(ruleset.Value.ShortName);
|
||||
}
|
||||
}
|
||||
|
||||
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case APIState.Online:
|
||||
fetchRecommendedValues();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Graphics.Containers
|
||||
/// </summary>
|
||||
public class ScalingContainer : Container
|
||||
{
|
||||
private const float duration = 500;
|
||||
internal const float TRANSITION_DURATION = 500;
|
||||
|
||||
private Bindable<float> sizeX;
|
||||
private Bindable<float> sizeY;
|
||||
@ -99,7 +99,7 @@ namespace osu.Game.Graphics.Containers
|
||||
if (applyUIScale)
|
||||
{
|
||||
uiScale = osuConfig.GetBindable<float>(OsuSetting.UIScale);
|
||||
uiScale.BindValueChanged(args => this.TransformTo(nameof(CurrentScale), args.NewValue, duration, Easing.OutQuart), true);
|
||||
uiScale.BindValueChanged(args => this.TransformTo(nameof(CurrentScale), args.NewValue, TRANSITION_DURATION, Easing.OutQuart), true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,10 +163,10 @@ namespace osu.Game.Graphics.Containers
|
||||
backgroundStack.Push(new ScalingBackgroundScreen());
|
||||
}
|
||||
|
||||
backgroundStack.FadeIn(duration);
|
||||
backgroundStack.FadeIn(TRANSITION_DURATION);
|
||||
}
|
||||
else
|
||||
backgroundStack?.FadeOut(duration);
|
||||
backgroundStack?.FadeOut(TRANSITION_DURATION);
|
||||
}
|
||||
|
||||
RectangleF targetRect = new RectangleF(Vector2.Zero, Vector2.One);
|
||||
@ -195,13 +195,13 @@ namespace osu.Game.Graphics.Containers
|
||||
if (requiresMasking)
|
||||
sizableContainer.Masking = true;
|
||||
|
||||
sizableContainer.MoveTo(targetRect.Location, duration, Easing.OutQuart);
|
||||
sizableContainer.ResizeTo(targetRect.Size, duration, Easing.OutQuart);
|
||||
sizableContainer.MoveTo(targetRect.Location, TRANSITION_DURATION, Easing.OutQuart);
|
||||
sizableContainer.ResizeTo(targetRect.Size, TRANSITION_DURATION, Easing.OutQuart);
|
||||
|
||||
// Of note, this will not work great in the case of nested ScalingContainers where multiple are applying corner radius.
|
||||
// Masking and corner radius should likely only be applied at one point in the full game stack to fix this.
|
||||
// An example of how this can occur is when the skin editor is visible and the game screen scaling is set to "Everything".
|
||||
sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, duration, requiresMasking ? Easing.OutQuart : Easing.None)
|
||||
sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, TRANSITION_DURATION, requiresMasking ? Easing.OutQuart : Easing.None)
|
||||
.OnComplete(_ => { sizableContainer.Masking = requiresMasking; });
|
||||
}
|
||||
|
||||
|
@ -2,11 +2,13 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -65,6 +67,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
get
|
||||
{
|
||||
if (BeatmapModelManager.VIDEO_EXTENSIONS.Contains(File.Extension))
|
||||
return FontAwesome.Regular.FileVideo;
|
||||
|
||||
switch (File.Extension)
|
||||
{
|
||||
case @".ogg":
|
||||
@ -77,12 +82,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
case @".png":
|
||||
return FontAwesome.Regular.FileImage;
|
||||
|
||||
case @".mp4":
|
||||
case @".avi":
|
||||
case @".mov":
|
||||
case @".flv":
|
||||
return FontAwesome.Regular.FileVideo;
|
||||
|
||||
default:
|
||||
return FontAwesome.Regular.File;
|
||||
}
|
||||
|
@ -30,12 +30,12 @@ namespace osu.Game.Localisation
|
||||
public static LocalisableString OutputDevice => new TranslatableString(getKey(@"output_device"), @"Output device");
|
||||
|
||||
/// <summary>
|
||||
/// "Master"
|
||||
/// "Hitsound stereo separation"
|
||||
/// </summary>
|
||||
public static LocalisableString PositionalLevel => new TranslatableString(getKey(@"positional_hitsound_audio_level"), @"Hitsound stereo separation");
|
||||
|
||||
/// <summary>
|
||||
/// "Level"
|
||||
/// "Master"
|
||||
/// </summary>
|
||||
public static LocalisableString MasterVolume => new TranslatableString(getKey(@"master_volume"), @"Master");
|
||||
|
||||
@ -69,6 +69,6 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString OffsetWizard => new TranslatableString(getKey(@"offset_wizard"), @"Offset wizard");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -210,7 +210,7 @@ namespace osu.Game.Localisation
|
||||
public static LocalisableString ToggleInGameInterface => new TranslatableString(getKey(@"toggle_in_game_interface"), @"Toggle in-game interface");
|
||||
|
||||
/// <summary>
|
||||
/// "Toggle Mod Select"
|
||||
/// "Toggle mod select"
|
||||
/// </summary>
|
||||
public static LocalisableString ToggleModSelection => new TranslatableString(getKey(@"toggle_mod_selection"), @"Toggle mod select");
|
||||
|
||||
@ -299,6 +299,6 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ToggleChatFocus => new TranslatableString(getKey(@"toggle_chat_focus"), @"Toggle chat focus");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -15,10 +15,10 @@ namespace osu.Game.Localisation
|
||||
public static LocalisableString JoystickGamepad => new TranslatableString(getKey(@"joystick_gamepad"), @"Joystick / Gamepad");
|
||||
|
||||
/// <summary>
|
||||
/// "Deadzone Threshold"
|
||||
/// "Deadzone"
|
||||
/// </summary>
|
||||
public static LocalisableString DeadzoneThreshold => new TranslatableString(getKey(@"deadzone_threshold"), @"Deadzone");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString DeleteAllBeatmaps => new TranslatableString(getKey(@"delete_all_beatmaps"), @"Delete ALL beatmaps");
|
||||
|
||||
/// <summary>
|
||||
/// "Delete ALL beatmap videos"
|
||||
/// </summary>
|
||||
public static LocalisableString DeleteAllBeatmapVideos => new TranslatableString(getKey(@"delete_all_beatmap_videos"), @"Delete ALL beatmap videos");
|
||||
|
||||
/// <summary>
|
||||
/// "Import scores from stable"
|
||||
/// </summary>
|
||||
|
@ -54,6 +54,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ExportSkinButton => new TranslatableString(getKey(@"export_skin_button"), @"Export selected skin");
|
||||
|
||||
/// <summary>
|
||||
/// "Delete selected skin"
|
||||
/// </summary>
|
||||
public static LocalisableString DeleteSkinButton => new TranslatableString(getKey(@"delete_skin_button"), @"Delete selected skin");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -61,8 +61,7 @@ namespace osu.Game.Online.Chat
|
||||
/// </summary>
|
||||
public IBindableList<Channel> AvailableChannels => availableChannels;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
private readonly IAPIProvider api;
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache users { get; set; }
|
||||
@ -71,8 +70,9 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
private readonly IBindable<bool> isIdle = new BindableBool();
|
||||
|
||||
public ChannelManager()
|
||||
public ChannelManager(IAPIProvider api)
|
||||
{
|
||||
this.api = api;
|
||||
CurrentChannel.ValueChanged += currentChannelChanged;
|
||||
}
|
||||
|
||||
|
@ -77,7 +77,7 @@ namespace osu.Game.Online.Chat
|
||||
if (!messages.Any())
|
||||
return;
|
||||
|
||||
var channel = channelManager.JoinedChannels.SingleOrDefault(c => c.Id == messages.First().ChannelId);
|
||||
var channel = channelManager.JoinedChannels.SingleOrDefault(c => c.Id > 0 && c.Id == messages.First().ChannelId);
|
||||
|
||||
if (channel == null)
|
||||
return;
|
||||
|
@ -155,39 +155,42 @@ namespace osu.Game.Online.Chat
|
||||
{
|
||||
public Func<Message, ChatLine> CreateChatLineAction;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
public StandAloneDrawableChannel(Channel channel)
|
||||
: base(channel)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
ChatLineFlow.Padding = new MarginPadding { Horizontal = 0 };
|
||||
}
|
||||
|
||||
protected override ChatLine CreateChatLine(Message m) => CreateChatLineAction(m);
|
||||
|
||||
protected override Drawable CreateDaySeparator(DateTimeOffset time) => new DaySeparator(time)
|
||||
protected override DaySeparator CreateDaySeparator(DateTimeOffset time) => new StandAloneDaySeparator(time);
|
||||
}
|
||||
|
||||
protected class StandAloneDaySeparator : DaySeparator
|
||||
{
|
||||
protected override float TextSize => 14;
|
||||
protected override float LineHeight => 1;
|
||||
protected override float Spacing => 5;
|
||||
protected override float DateAlign => 125;
|
||||
|
||||
public StandAloneDaySeparator(DateTimeOffset time)
|
||||
: base(time)
|
||||
{
|
||||
TextSize = 14,
|
||||
Colour = colours.Yellow,
|
||||
LineHeight = 1,
|
||||
Padding = new MarginPadding { Horizontal = 10 },
|
||||
Margin = new MarginPadding { Vertical = 5 },
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Height = 25;
|
||||
Colour = colours.Yellow;
|
||||
}
|
||||
}
|
||||
|
||||
protected class StandAloneMessage : ChatLine
|
||||
{
|
||||
protected override float TextSize => 15;
|
||||
|
||||
protected override float HorizontalPadding => 10;
|
||||
protected override float MessagePadding => 120;
|
||||
protected override float TimestampPadding => 50;
|
||||
protected override float Spacing => 5;
|
||||
protected override float TimestampWidth => 45;
|
||||
protected override float UsernameWidth => 75;
|
||||
|
||||
public StandAloneMessage(Message message)
|
||||
: base(message)
|
||||
|
@ -39,7 +39,7 @@ namespace osu.Game.Online.Spectator
|
||||
/// <summary>
|
||||
/// The states of all users currently being watched.
|
||||
/// </summary>
|
||||
public IBindableDictionary<int, SpectatorState> WatchedUserStates => watchedUserStates;
|
||||
public virtual IBindableDictionary<int, SpectatorState> WatchedUserStates => watchedUserStates;
|
||||
|
||||
/// <summary>
|
||||
/// A global list of all players currently playing.
|
||||
@ -172,6 +172,7 @@ namespace osu.Game.Online.Spectator
|
||||
currentState.RulesetID = score.ScoreInfo.RulesetID;
|
||||
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
|
||||
currentState.State = SpectatedUserState.Playing;
|
||||
currentState.MaximumScoringValues = state.ScoreProcessor.MaximumScoringValues;
|
||||
|
||||
currentBeatmap = state.Beatmap;
|
||||
currentScore = score;
|
||||
|
192
osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
Normal file
192
osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
Normal file
@ -0,0 +1,192 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Online.Spectator
|
||||
{
|
||||
/// <summary>
|
||||
/// A wrapper over a <see cref="ScoreProcessor"/> for spectated users.
|
||||
/// This should be used when a local "playable" beatmap is unavailable or expensive to generate for the spectated user.
|
||||
/// </summary>
|
||||
public class SpectatorScoreProcessor : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The current total score.
|
||||
/// </summary>
|
||||
public readonly BindableDouble TotalScore = new BindableDouble { MinValue = 0 };
|
||||
|
||||
/// <summary>
|
||||
/// The current accuracy.
|
||||
/// </summary>
|
||||
public readonly BindableDouble Accuracy = new BindableDouble(1) { MinValue = 0, MaxValue = 1 };
|
||||
|
||||
/// <summary>
|
||||
/// The current combo.
|
||||
/// </summary>
|
||||
public readonly BindableInt Combo = new BindableInt();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ScoringMode"/> used to calculate scores.
|
||||
/// </summary>
|
||||
public readonly Bindable<ScoringMode> Mode = new Bindable<ScoringMode>();
|
||||
|
||||
/// <summary>
|
||||
/// The applied <see cref="Mod"/>s.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Mod> Mods => scoreProcessor?.Mods.Value ?? Array.Empty<Mod>();
|
||||
|
||||
private IClock? referenceClock;
|
||||
|
||||
/// <summary>
|
||||
/// The clock used to determine the current score.
|
||||
/// </summary>
|
||||
public IClock ReferenceClock
|
||||
{
|
||||
get => referenceClock ?? Clock;
|
||||
set => referenceClock = value;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private SpectatorClient spectatorClient { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesetStore { get; set; } = null!;
|
||||
|
||||
private readonly IBindableDictionary<int, SpectatorState> spectatorStates = new BindableDictionary<int, SpectatorState>();
|
||||
private readonly List<TimedFrame> replayFrames = new List<TimedFrame>();
|
||||
private readonly int userId;
|
||||
|
||||
private SpectatorState? spectatorState;
|
||||
private ScoreProcessor? scoreProcessor;
|
||||
private ScoreInfo? scoreInfo;
|
||||
|
||||
public SpectatorScoreProcessor(int userId)
|
||||
{
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Mode.BindValueChanged(_ => UpdateScore());
|
||||
|
||||
spectatorStates.BindTo(spectatorClient.WatchedUserStates);
|
||||
spectatorStates.BindCollectionChanged(onSpectatorStatesChanged, true);
|
||||
|
||||
spectatorClient.OnNewFrames += onNewFrames;
|
||||
}
|
||||
|
||||
private void onSpectatorStatesChanged(object? sender, NotifyDictionaryChangedEventArgs<int, SpectatorState> e)
|
||||
{
|
||||
if (!spectatorStates.TryGetValue(userId, out var userState) || userState.BeatmapID == null || userState.RulesetID == null)
|
||||
{
|
||||
scoreProcessor?.RemoveAndDisposeImmediately();
|
||||
scoreProcessor = null;
|
||||
scoreInfo = null;
|
||||
spectatorState = null;
|
||||
replayFrames.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (scoreProcessor != null)
|
||||
return;
|
||||
|
||||
Debug.Assert(scoreInfo == null);
|
||||
|
||||
RulesetInfo? rulesetInfo = rulesetStore.GetRuleset(userState.RulesetID.Value);
|
||||
if (rulesetInfo == null)
|
||||
return;
|
||||
|
||||
Ruleset ruleset = rulesetInfo.CreateInstance();
|
||||
|
||||
spectatorState = userState;
|
||||
scoreInfo = new ScoreInfo { Ruleset = rulesetInfo };
|
||||
scoreProcessor = ruleset.CreateScoreProcessor();
|
||||
scoreProcessor.Mods.Value = userState.Mods.Select(m => m.ToMod(ruleset)).ToArray();
|
||||
}
|
||||
|
||||
private void onNewFrames(int incomingUserId, FrameDataBundle bundle)
|
||||
{
|
||||
if (incomingUserId != userId)
|
||||
return;
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
if (scoreProcessor == null)
|
||||
return;
|
||||
|
||||
replayFrames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header));
|
||||
UpdateScore();
|
||||
});
|
||||
}
|
||||
|
||||
public void UpdateScore()
|
||||
{
|
||||
if (scoreInfo == null || replayFrames.Count == 0)
|
||||
return;
|
||||
|
||||
Debug.Assert(spectatorState != null);
|
||||
Debug.Assert(scoreProcessor != null);
|
||||
|
||||
int frameIndex = replayFrames.BinarySearch(new TimedFrame(ReferenceClock.CurrentTime));
|
||||
if (frameIndex < 0)
|
||||
frameIndex = ~frameIndex;
|
||||
frameIndex = Math.Clamp(frameIndex - 1, 0, replayFrames.Count - 1);
|
||||
|
||||
TimedFrame frame = replayFrames[frameIndex];
|
||||
Debug.Assert(frame.Header != null);
|
||||
|
||||
scoreInfo.MaxCombo = frame.Header.MaxCombo;
|
||||
scoreInfo.Statistics = frame.Header.Statistics;
|
||||
|
||||
Accuracy.Value = frame.Header.Accuracy;
|
||||
Combo.Value = frame.Header.Combo;
|
||||
|
||||
scoreProcessor.ExtractScoringValues(frame.Header, out var currentScoringValues, out _);
|
||||
TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, currentScoringValues, spectatorState.MaximumScoringValues);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (spectatorClient.IsNotNull())
|
||||
spectatorClient.OnNewFrames -= onNewFrames;
|
||||
}
|
||||
|
||||
private class TimedFrame : IComparable<TimedFrame>
|
||||
{
|
||||
public readonly double Time;
|
||||
public readonly FrameHeader? Header;
|
||||
|
||||
public TimedFrame(double time)
|
||||
{
|
||||
Time = time;
|
||||
}
|
||||
|
||||
public TimedFrame(double time, FrameHeader header)
|
||||
{
|
||||
Time = time;
|
||||
Header = header;
|
||||
}
|
||||
|
||||
public int CompareTo(TimedFrame other) => Time.CompareTo(other.Time);
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using MessagePack;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Online.Spectator
|
||||
{
|
||||
@ -27,6 +28,9 @@ namespace osu.Game.Online.Spectator
|
||||
[Key(3)]
|
||||
public SpectatedUserState State { get; set; }
|
||||
|
||||
[Key(4)]
|
||||
public ScoringValues MaximumScoringValues { get; set; }
|
||||
|
||||
public bool Equals(SpectatorState other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
|
@ -851,7 +851,7 @@ namespace osu.Game
|
||||
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
|
||||
loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true);
|
||||
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
|
||||
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
|
||||
loadComponentSingleFile(channelManager = new ChannelManager(API), AddInternal, true);
|
||||
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
|
||||
loadComponentSingleFile(new MessageNotifier(), AddInternal, true);
|
||||
loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true);
|
||||
|
@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
@ -26,42 +28,6 @@ namespace osu.Game.Overlays.Chat
|
||||
{
|
||||
public class ChatLine : CompositeDrawable
|
||||
{
|
||||
public const float LEFT_PADDING = default_message_padding + default_horizontal_padding * 2;
|
||||
|
||||
private const float default_message_padding = 200;
|
||||
|
||||
protected virtual float MessagePadding => default_message_padding;
|
||||
|
||||
private const float default_timestamp_padding = 65;
|
||||
|
||||
protected virtual float TimestampPadding => default_timestamp_padding;
|
||||
|
||||
private const float default_horizontal_padding = 15;
|
||||
|
||||
protected virtual float HorizontalPadding => default_horizontal_padding;
|
||||
|
||||
protected virtual float TextSize => 20;
|
||||
|
||||
private Color4 usernameColour;
|
||||
|
||||
private OsuSpriteText timestamp;
|
||||
|
||||
public ChatLine(Message message)
|
||||
{
|
||||
Message = message;
|
||||
Padding = new MarginPadding { Left = HorizontalPadding, Right = HorizontalPadding };
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
}
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private ChannelManager chatManager { get; set; }
|
||||
|
||||
private Message message;
|
||||
private OsuSpriteText username;
|
||||
|
||||
public LinkFlowContainer ContentFlow { get; private set; }
|
||||
|
||||
public Message Message
|
||||
{
|
||||
get => message;
|
||||
@ -78,119 +44,101 @@ namespace osu.Game.Overlays.Chat
|
||||
}
|
||||
}
|
||||
|
||||
public LinkFlowContainer ContentFlow { get; private set; } = null!;
|
||||
|
||||
protected virtual float TextSize => 20;
|
||||
|
||||
protected virtual float Spacing => 15;
|
||||
|
||||
protected virtual float TimestampWidth => 60;
|
||||
|
||||
protected virtual float UsernameWidth => 130;
|
||||
|
||||
private Color4 usernameColour;
|
||||
|
||||
private OsuSpriteText timestamp = null!;
|
||||
|
||||
private Message message = null!;
|
||||
|
||||
private OsuSpriteText username = null!;
|
||||
|
||||
private Container? highlight;
|
||||
|
||||
private bool senderHasColour => !string.IsNullOrEmpty(message.Sender.Colour);
|
||||
|
||||
private bool messageHasColour => Message.IsAction && senderHasColour;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
private ChannelManager? chatManager { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
public ChatLine(Message message)
|
||||
{
|
||||
Message = message;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(OverlayColourProvider? colourProvider)
|
||||
{
|
||||
usernameColour = senderHasColour
|
||||
? Color4Extensions.FromHex(message.Sender.Colour)
|
||||
: username_colours[message.Sender.Id % username_colours.Length];
|
||||
|
||||
Drawable effectedUsername = username = new OsuSpriteText
|
||||
InternalChild = new GridContainer
|
||||
{
|
||||
Shadow = false,
|
||||
Colour = senderHasColour ? colours.ChatBlue : usernameColour,
|
||||
Truncate = true,
|
||||
EllipsisString = "… :",
|
||||
Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Bold, italics: true),
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
MaxWidth = MessagePadding - TimestampPadding
|
||||
};
|
||||
|
||||
if (senderHasColour)
|
||||
{
|
||||
// Background effect
|
||||
effectedUsername = new Container
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 4,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Roundness = 1,
|
||||
Radius = 1,
|
||||
Colour = Color4.Black.Opacity(0.3f),
|
||||
Offset = new Vector2(0, 1),
|
||||
Type = EdgeEffectType.Shadow,
|
||||
},
|
||||
Child = new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Y = 0,
|
||||
Masking = true,
|
||||
CornerRadius = 4,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = usernameColour,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = 4, Right = 4, Bottom = 1, Top = -2 },
|
||||
Child = username
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Size = new Vector2(MessagePadding, TextSize),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
timestamp = new OsuSpriteText
|
||||
{
|
||||
Shadow = false,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.GetFont(size: TextSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true)
|
||||
},
|
||||
new MessageSender(message.Sender)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Origin = Anchor.TopRight,
|
||||
Anchor = Anchor.TopRight,
|
||||
Child = effectedUsername,
|
||||
},
|
||||
}
|
||||
new Dimension(GridSizeMode.Absolute, TimestampWidth + Spacing + UsernameWidth + Spacing),
|
||||
new Dimension(),
|
||||
},
|
||||
new Container
|
||||
Content = new[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Left = MessagePadding + HorizontalPadding },
|
||||
Children = new Drawable[]
|
||||
new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
timestamp = new OsuSpriteText
|
||||
{
|
||||
Shadow = false,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.GetFont(size: TextSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true),
|
||||
MaxWidth = TimestampWidth,
|
||||
Colour = colourProvider?.Background1 ?? Colour4.White,
|
||||
},
|
||||
new MessageSender(message.Sender)
|
||||
{
|
||||
Width = UsernameWidth,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Origin = Anchor.TopRight,
|
||||
Anchor = Anchor.TopRight,
|
||||
Child = createUsername(),
|
||||
Margin = new MarginPadding { Horizontal = Spacing },
|
||||
},
|
||||
},
|
||||
},
|
||||
ContentFlow = new LinkFlowContainer(t =>
|
||||
{
|
||||
t.Shadow = false;
|
||||
|
||||
if (Message.IsAction)
|
||||
{
|
||||
t.Font = OsuFont.GetFont(italics: true);
|
||||
|
||||
if (senderHasColour)
|
||||
t.Colour = Color4Extensions.FromHex(message.Sender.Colour);
|
||||
}
|
||||
|
||||
t.Font = t.Font.With(size: TextSize);
|
||||
t.Font = t.Font.With(size: TextSize, italics: Message.IsAction);
|
||||
t.Colour = messageHasColour ? Color4Extensions.FromHex(message.Sender.Colour) : colourProvider?.Content1 ?? Colour4.White;
|
||||
})
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -203,8 +151,6 @@ namespace osu.Game.Overlays.Chat
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
private Container highlight;
|
||||
|
||||
/// <summary>
|
||||
/// Performs a highlight animation on this <see cref="ChatLine"/>.
|
||||
/// </summary>
|
||||
@ -233,7 +179,7 @@ namespace osu.Game.Overlays.Chat
|
||||
timestamp.FadeTo(message is LocalEchoMessage ? 0 : 1, 500, Easing.OutQuint);
|
||||
|
||||
timestamp.Text = $@"{message.Timestamp.LocalDateTime:HH:mm:ss}";
|
||||
username.Text = $@"{message.Sender.Username}" + (senderHasColour || message.IsAction ? "" : ":");
|
||||
username.Text = $@"{message.Sender.Username}";
|
||||
|
||||
// remove non-existent channels from the link list
|
||||
message.Links.RemoveAll(link => link.Action == LinkAction.OpenChannel && chatManager?.AvailableChannels.Any(c => c.Name == link.Argument.ToString()) != true);
|
||||
@ -242,22 +188,78 @@ namespace osu.Game.Overlays.Chat
|
||||
ContentFlow.AddLinks(message.DisplayContent, message.Links);
|
||||
}
|
||||
|
||||
private Drawable createUsername()
|
||||
{
|
||||
username = new OsuSpriteText
|
||||
{
|
||||
Shadow = false,
|
||||
Colour = senderHasColour ? colours.ChatBlue : usernameColour,
|
||||
Truncate = true,
|
||||
EllipsisString = "…",
|
||||
Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Bold, italics: true),
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
MaxWidth = UsernameWidth,
|
||||
};
|
||||
|
||||
if (!senderHasColour)
|
||||
return username;
|
||||
|
||||
// Background effect
|
||||
return new Container
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 4,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Roundness = 1,
|
||||
Radius = 1,
|
||||
Colour = Color4.Black.Opacity(0.3f),
|
||||
Offset = new Vector2(0, 1),
|
||||
Type = EdgeEffectType.Shadow,
|
||||
},
|
||||
Child = new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 4,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = usernameColour,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = 4, Right = 4, Bottom = 1, Top = -2 },
|
||||
Child = username
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private class MessageSender : OsuClickableContainer, IHasContextMenu
|
||||
{
|
||||
private readonly APIUser sender;
|
||||
|
||||
private Action startChatAction;
|
||||
private Action startChatAction = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
public MessageSender(APIUser sender)
|
||||
{
|
||||
this.sender = sender;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(UserProfileOverlay profile, ChannelManager chatManager)
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(UserProfileOverlay? profile, ChannelManager? chatManager)
|
||||
{
|
||||
Action = () => profile?.ShowUser(sender);
|
||||
startChatAction = () => chatManager?.OpenPrivateChannel(sender);
|
||||
|
@ -1,109 +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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Chat;
|
||||
|
||||
namespace osu.Game.Overlays.Chat
|
||||
{
|
||||
public class ChatOverlayDrawableChannel : DrawableChannel
|
||||
{
|
||||
public ChatOverlayDrawableChannel(Channel channel)
|
||||
: base(channel)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
ChatLineFlow.Padding = new MarginPadding(0);
|
||||
}
|
||||
|
||||
protected override Drawable CreateDaySeparator(DateTimeOffset time) => new ChatOverlayDaySeparator(time);
|
||||
|
||||
private class ChatOverlayDaySeparator : Container
|
||||
{
|
||||
private readonly DateTimeOffset time;
|
||||
|
||||
public ChatOverlayDaySeparator(DateTimeOffset time)
|
||||
{
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Padding = new MarginPadding { Horizontal = 15, Vertical = 20 };
|
||||
Child = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.Absolute, 200),
|
||||
new Dimension(GridSizeMode.Absolute, 15),
|
||||
new Dimension(),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 15),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new Circle
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Colour = colourProvider.Background5,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 2,
|
||||
},
|
||||
Drawable.Empty(),
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Text = time.ToLocalTime().ToString("dd MMMM yyyy").ToUpper(),
|
||||
Font = OsuFont.Torus.With(size: 15, weight: FontWeight.SemiBold),
|
||||
Colour = colourProvider.Content1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Drawable.Empty(),
|
||||
new Circle
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Colour = colourProvider.Background5,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
105
osu.Game/Overlays/Chat/DaySeparator.cs
Normal file
105
osu.Game/Overlays/Chat/DaySeparator.cs
Normal file
@ -0,0 +1,105 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Overlays.Chat
|
||||
{
|
||||
public class DaySeparator : Container
|
||||
{
|
||||
protected virtual float TextSize => 15;
|
||||
|
||||
protected virtual float LineHeight => 2;
|
||||
|
||||
protected virtual float DateAlign => 205;
|
||||
|
||||
protected virtual float Spacing => 15;
|
||||
|
||||
private readonly DateTimeOffset time;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private OverlayColourProvider? colourProvider { get; set; }
|
||||
|
||||
public DaySeparator(DateTimeOffset time)
|
||||
{
|
||||
this.time = time;
|
||||
Height = 40;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Child = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
RowDimensions = new[] { new Dimension() },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.Absolute, DateAlign),
|
||||
new Dimension(GridSizeMode.Absolute, Spacing),
|
||||
new Dimension(),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions = new[] { new Dimension() },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, Spacing),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = LineHeight,
|
||||
Colour = colourProvider?.Background5 ?? Colour4.White,
|
||||
},
|
||||
Drawable.Empty(),
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Text = time.ToLocalTime().ToString("dd MMMM yyyy").ToUpper(),
|
||||
Font = OsuFont.Torus.With(size: TextSize, weight: FontWeight.SemiBold),
|
||||
Colour = colourProvider?.Content1 ?? Colour4.White,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
Drawable.Empty(),
|
||||
new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = LineHeight,
|
||||
Colour = colourProvider?.Background5 ?? Colour4.White,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -7,14 +7,9 @@ using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Chat;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -40,9 +35,6 @@ namespace osu.Game.Overlays.Chat
|
||||
}
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
public DrawableChannel(Channel channel)
|
||||
{
|
||||
Channel = channel;
|
||||
@ -67,7 +59,7 @@ namespace osu.Game.Overlays.Chat
|
||||
Padding = new MarginPadding { Bottom = 5 },
|
||||
Child = ChatLineFlow = new FillFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding { Left = 20, Right = 20 },
|
||||
Padding = new MarginPadding { Horizontal = 10 },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
@ -121,11 +113,7 @@ namespace osu.Game.Overlays.Chat
|
||||
|
||||
protected virtual ChatLine CreateChatLine(Message m) => new ChatLine(m);
|
||||
|
||||
protected virtual Drawable CreateDaySeparator(DateTimeOffset time) => new DaySeparator(time)
|
||||
{
|
||||
Colour = colours.ChatBlue.Lighten(0.7f),
|
||||
Margin = new MarginPadding { Vertical = 10 },
|
||||
};
|
||||
protected virtual DaySeparator CreateDaySeparator(DateTimeOffset time) => new DaySeparator(time);
|
||||
|
||||
private void newMessagesArrived(IEnumerable<Message> newMessages) => Schedule(() =>
|
||||
{
|
||||
@ -203,69 +191,5 @@ namespace osu.Game.Overlays.Chat
|
||||
});
|
||||
|
||||
private IEnumerable<ChatLine> chatLines => ChatLineFlow.Children.OfType<ChatLine>();
|
||||
|
||||
public class DaySeparator : Container
|
||||
{
|
||||
public float TextSize
|
||||
{
|
||||
get => text.Font.Size;
|
||||
set => text.Font = text.Font.With(size: value);
|
||||
}
|
||||
|
||||
private float lineHeight = 2;
|
||||
|
||||
public float LineHeight
|
||||
{
|
||||
get => lineHeight;
|
||||
set => lineHeight = leftBox.Height = rightBox.Height = value;
|
||||
}
|
||||
|
||||
private readonly SpriteText text;
|
||||
private readonly Box leftBox;
|
||||
private readonly Box rightBox;
|
||||
|
||||
public DaySeparator(DateTimeOffset time)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Child = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(),
|
||||
},
|
||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), },
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
leftBox = new Box
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = lineHeight,
|
||||
},
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
Margin = new MarginPadding { Horizontal = 10 },
|
||||
Text = time.ToLocalTime().ToString("dd MMM yyyy"),
|
||||
},
|
||||
rightBox = new Box
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = lineHeight,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,9 +38,9 @@ namespace osu.Game.Overlays
|
||||
private LoadingLayer loading = null!;
|
||||
private ChannelListing channelListing = null!;
|
||||
private ChatTextBar textBar = null!;
|
||||
private Container<ChatOverlayDrawableChannel> currentChannelContainer = null!;
|
||||
private Container<DrawableChannel> currentChannelContainer = null!;
|
||||
|
||||
private readonly Dictionary<Channel, ChatOverlayDrawableChannel> loadedChannels = new Dictionary<Channel, ChatOverlayDrawableChannel>();
|
||||
private readonly Dictionary<Channel, DrawableChannel> loadedChannels = new Dictionary<Channel, DrawableChannel>();
|
||||
|
||||
protected IEnumerable<DrawableChannel> DrawableChannels => loadedChannels.Values;
|
||||
|
||||
@ -126,7 +126,7 @@ namespace osu.Game.Overlays
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background4,
|
||||
},
|
||||
currentChannelContainer = new Container<ChatOverlayDrawableChannel>
|
||||
currentChannelContainer = new Container<DrawableChannel>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
@ -313,7 +313,7 @@ namespace osu.Game.Overlays
|
||||
loading.Show();
|
||||
|
||||
// Ensure the drawable channel is stored before async load to prevent double loading
|
||||
ChatOverlayDrawableChannel drawableChannel = CreateDrawableChannel(newChannel);
|
||||
DrawableChannel drawableChannel = CreateDrawableChannel(newChannel);
|
||||
loadedChannels.Add(newChannel, drawableChannel);
|
||||
|
||||
LoadComponentAsync(drawableChannel, loadedDrawable =>
|
||||
@ -338,7 +338,7 @@ namespace osu.Game.Overlays
|
||||
channelManager.MarkChannelAsRead(newChannel);
|
||||
}
|
||||
|
||||
protected virtual ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) => new ChatOverlayDrawableChannel(newChannel);
|
||||
protected virtual DrawableChannel CreateDrawableChannel(Channel newChannel) => new DrawableChannel(newChannel);
|
||||
|
||||
private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
|
||||
{
|
||||
@ -361,7 +361,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
if (loadedChannels.ContainsKey(channel))
|
||||
{
|
||||
ChatOverlayDrawableChannel loaded = loadedChannels[channel];
|
||||
DrawableChannel loaded = loadedChannels[channel];
|
||||
loadedChannels.Remove(channel);
|
||||
// DrawableChannel removed from cache must be manually disposed
|
||||
loaded.Dispose();
|
||||
|
@ -126,6 +126,7 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
private class SampleScreenContainer : CompositeDrawable
|
||||
{
|
||||
private readonly OsuScreen screen;
|
||||
|
||||
// Minimal isolation from main game.
|
||||
|
||||
[Cached]
|
||||
@ -151,6 +152,9 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
|
||||
new DependencyContainer(new DependencyIsolationContainer(base.CreateChildDependencies(parent)));
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio, TextureStore textures, RulesetStore rulesets)
|
||||
{
|
||||
@ -197,5 +201,41 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
stack.PushSynchronously(screen);
|
||||
}
|
||||
}
|
||||
|
||||
private class DependencyIsolationContainer : IReadOnlyDependencyContainer
|
||||
{
|
||||
private readonly IReadOnlyDependencyContainer parentDependencies;
|
||||
|
||||
private readonly Type[] isolatedTypes =
|
||||
{
|
||||
typeof(OsuGame)
|
||||
};
|
||||
|
||||
public DependencyIsolationContainer(IReadOnlyDependencyContainer parentDependencies)
|
||||
{
|
||||
this.parentDependencies = parentDependencies;
|
||||
}
|
||||
|
||||
public object Get(Type type)
|
||||
{
|
||||
if (isolatedTypes.Contains(type))
|
||||
return null;
|
||||
|
||||
return parentDependencies.Get(type);
|
||||
}
|
||||
|
||||
public object Get(Type type, CacheInfo info)
|
||||
{
|
||||
if (isolatedTypes.Contains(type))
|
||||
return null;
|
||||
|
||||
return parentDependencies.Get(type, info);
|
||||
}
|
||||
|
||||
public void Inject<T>(T instance) where T : class
|
||||
{
|
||||
parentDependencies.Inject(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
private SettingsButton deleteSkinsButton;
|
||||
private SettingsButton restoreButton;
|
||||
private SettingsButton undeleteButton;
|
||||
private SettingsButton deleteBeatmapVideosButton;
|
||||
|
||||
[BackgroundDependencyLoader(permitNulls: true)]
|
||||
private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, [CanBeNull] LegacyImportManager legacyImportManager, IDialogOverlay dialogOverlay)
|
||||
@ -58,6 +59,19 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
}
|
||||
});
|
||||
|
||||
Add(deleteBeatmapVideosButton = new DangerousSettingsButton
|
||||
{
|
||||
Text = MaintenanceSettingsStrings.DeleteAllBeatmapVideos,
|
||||
Action = () =>
|
||||
{
|
||||
dialogOverlay?.Push(new MassVideoDeleteConfirmationDialog(() =>
|
||||
{
|
||||
deleteBeatmapVideosButton.Enabled.Value = false;
|
||||
Task.Run(beatmaps.DeleteAllVideos).ContinueWith(t => Schedule(() => deleteBeatmapVideosButton.Enabled.Value = true));
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
if (legacyImportManager?.SupportsImportFromStable == true)
|
||||
{
|
||||
Add(importScoresButton = new SettingsButton
|
||||
|
@ -0,0 +1,16 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
public class MassVideoDeleteConfirmationDialog : MassDeleteConfirmationDialog
|
||||
{
|
||||
public MassVideoDeleteConfirmationDialog(Action deleteAction)
|
||||
: base(deleteAction)
|
||||
{
|
||||
BodyText = "All beatmap videos? This cannot be undone!";
|
||||
}
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Skinning.Editor;
|
||||
using Realms;
|
||||
@ -67,6 +68,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
Action = () => skinEditor?.ToggleVisibility(),
|
||||
},
|
||||
new ExportSkinButton(),
|
||||
new DeleteSkinButton(),
|
||||
};
|
||||
|
||||
config.BindWith(OsuSetting.Skin, configBindable);
|
||||
@ -202,5 +204,36 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DeleteSkinButton : DangerousSettingsButton
|
||||
{
|
||||
[Resolved]
|
||||
private SkinManager skins { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDialogOverlay dialogOverlay { get; set; }
|
||||
|
||||
private Bindable<Skin> currentSkin;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Text = SkinSettingsStrings.DeleteSkinButton;
|
||||
Action = delete;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
currentSkin = skins.CurrentSkin.GetBoundCopy();
|
||||
currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => !s.Protected), true);
|
||||
}
|
||||
|
||||
private void delete()
|
||||
{
|
||||
dialogOverlay?.Push(new SkinDeleteDialog(currentSkin.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Utils;
|
||||
@ -28,7 +27,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.Volume
|
||||
{
|
||||
public class VolumeMeter : Container, IKeyBindingHandler<GlobalAction>, IStateful<SelectionState>
|
||||
public class VolumeMeter : Container, IStateful<SelectionState>
|
||||
{
|
||||
private CircularProgress volumeCircle;
|
||||
private CircularProgress volumeCircleGlow;
|
||||
@ -80,7 +79,7 @@ namespace osu.Game.Overlays.Volume
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, AudioManager audio)
|
||||
{
|
||||
hoverSample = audio.Samples.Get($"UI/{HoverSampleSet.Button.GetDescription()}-hover");
|
||||
hoverSample = audio.Samples.Get($@"UI/{HoverSampleSet.Button.GetDescription()}-hover");
|
||||
notchSample = audio.Samples.Get(@"UI/notch-tick");
|
||||
sampleLastPlaybackTime = Time.Current;
|
||||
|
||||
@ -132,7 +131,7 @@ namespace osu.Game.Overlays.Volume
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Name = "Progress under covers for smoothing",
|
||||
Name = @"Progress under covers for smoothing",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Rotation = 180,
|
||||
Child = volumeCircle = new CircularProgress
|
||||
@ -144,7 +143,7 @@ namespace osu.Game.Overlays.Volume
|
||||
},
|
||||
new Circle
|
||||
{
|
||||
Name = "Inner Cover",
|
||||
Name = @"Inner Cover",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -153,7 +152,7 @@ namespace osu.Game.Overlays.Volume
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Name = "Progress overlay for glow",
|
||||
Name = @"Progress overlay for glow",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -365,27 +364,6 @@ namespace osu.Game.Overlays.Volume
|
||||
{
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (!IsHovered)
|
||||
return false;
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.SelectPreviousGroup:
|
||||
State = SelectionState.Selected;
|
||||
adjust(1, false);
|
||||
return true;
|
||||
|
||||
case GlobalAction.SelectNextGroup:
|
||||
State = SelectionState.Selected;
|
||||
adjust(-1, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ using osu.Game.Input.Bindings;
|
||||
using osu.Game.Overlays.Volume;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
@ -171,6 +172,30 @@ namespace osu.Game.Overlays
|
||||
return base.OnMouseMove(e);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Left:
|
||||
Adjust(GlobalAction.PreviousVolumeMeter);
|
||||
return true;
|
||||
|
||||
case Key.Right:
|
||||
Adjust(GlobalAction.NextVolumeMeter);
|
||||
return true;
|
||||
|
||||
case Key.Down:
|
||||
Adjust(GlobalAction.DecreaseVolume);
|
||||
return true;
|
||||
|
||||
case Key.Up:
|
||||
Adjust(GlobalAction.IncreaseVolume);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
schedulePopOut();
|
||||
|
@ -460,6 +460,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
currentMaximumScoringValues.BaseScore = maximum.BaseScore;
|
||||
currentMaximumScoringValues.MaxCombo = maximum.MaxCombo;
|
||||
|
||||
Combo.Value = frame.Header.Combo;
|
||||
HighestCombo.Value = frame.Header.MaxCombo;
|
||||
|
||||
scoreResultCounts.Clear();
|
||||
|
@ -16,13 +16,15 @@ namespace osu.Game.Screens.Edit.Components
|
||||
/// </summary>
|
||||
internal class EditorSidebar : Container<EditorSidebarSection>
|
||||
{
|
||||
public const float WIDTH = 250;
|
||||
|
||||
private readonly Box background;
|
||||
|
||||
protected override Container<EditorSidebarSection> Content { get; }
|
||||
|
||||
public EditorSidebar()
|
||||
{
|
||||
Width = 250;
|
||||
Width = WIDTH;
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
|
@ -82,7 +82,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true);
|
||||
|
||||
// todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area.
|
||||
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(GameplayState.Ruleset.RulesetInfo, ScoreProcessor, users), l =>
|
||||
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(users), l =>
|
||||
{
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
|
@ -2,19 +2,16 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
public class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard
|
||||
{
|
||||
public MultiSpectatorLeaderboard(RulesetInfo ruleset, [NotNull] ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users)
|
||||
: base(ruleset, scoreProcessor, users)
|
||||
public MultiSpectatorLeaderboard(MultiplayerRoomUser[] users)
|
||||
: base(users)
|
||||
{
|
||||
}
|
||||
|
||||
@ -23,52 +20,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
if (!UserScores.TryGetValue(userId, out var data))
|
||||
throw new ArgumentException(@"Provided user is not tracked by this leaderboard", nameof(userId));
|
||||
|
||||
((SpectatingTrackedUserData)data).Clock = clock;
|
||||
data.ScoreProcessor.ReferenceClock = clock;
|
||||
}
|
||||
|
||||
public void RemoveClock(int userId)
|
||||
{
|
||||
if (!UserScores.TryGetValue(userId, out var data))
|
||||
throw new ArgumentException(@"Provided user is not tracked by this leaderboard", nameof(userId));
|
||||
|
||||
((SpectatingTrackedUserData)data).Clock = null;
|
||||
}
|
||||
|
||||
protected override TrackedUserData CreateUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(user, ruleset, scoreProcessor);
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
foreach (var (_, data) in UserScores)
|
||||
data.UpdateScore();
|
||||
}
|
||||
|
||||
private class SpectatingTrackedUserData : TrackedUserData
|
||||
{
|
||||
[CanBeNull]
|
||||
public IClock Clock;
|
||||
|
||||
public SpectatingTrackedUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor)
|
||||
: base(user, ruleset, scoreProcessor)
|
||||
{
|
||||
}
|
||||
|
||||
public override void UpdateScore()
|
||||
{
|
||||
if (Frames.Count == 0)
|
||||
return;
|
||||
|
||||
if (Clock == null)
|
||||
return;
|
||||
|
||||
int frameIndex = Frames.BinarySearch(new TimedFrame(Clock.CurrentTime));
|
||||
if (frameIndex < 0)
|
||||
frameIndex = ~frameIndex;
|
||||
frameIndex = Math.Clamp(frameIndex - 1, 0, Frames.Count - 1);
|
||||
|
||||
SetFrame(Frames[frameIndex]);
|
||||
}
|
||||
data.ScoreProcessor.UpdateScore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -128,12 +128,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
syncManager.AddPlayerClock(instances[i].GameplayClock);
|
||||
}
|
||||
|
||||
// Todo: This is not quite correct - it should be per-user to adjust for other mod combinations.
|
||||
var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
|
||||
var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor();
|
||||
scoreProcessor.ApplyBeatmap(playableBeatmap);
|
||||
|
||||
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(Ruleset.Value, scoreProcessor, users)
|
||||
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(users)
|
||||
{
|
||||
Expanded = { Value = true },
|
||||
}, l =>
|
||||
@ -240,7 +235,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
|
||||
instance.FadeColour(colours.Gray4, 400, Easing.OutQuint);
|
||||
syncManager.RemovePlayerClock(instance.GameplayClock);
|
||||
leaderboard.RemoveClock(userId);
|
||||
}
|
||||
|
||||
public override bool OnBackButton()
|
||||
|
@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Bindables;
|
||||
@ -8,10 +10,9 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
/// <summary>
|
||||
@ -39,6 +40,8 @@ namespace osu.Game.Screens.Play
|
||||
/// </summary>
|
||||
public readonly Score Score;
|
||||
|
||||
public readonly ScoreProcessor ScoreProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Whether gameplay completed without the user failing.
|
||||
/// </summary>
|
||||
@ -61,7 +64,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
|
||||
|
||||
public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList<Mod>? mods = null, Score? score = null)
|
||||
public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList<Mod>? mods = null, Score? score = null, ScoreProcessor? scoreProcessor = null)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
Ruleset = ruleset;
|
||||
@ -72,7 +75,8 @@ namespace osu.Game.Screens.Play
|
||||
Ruleset = ruleset.RulesetInfo
|
||||
}
|
||||
};
|
||||
Mods = mods ?? ArraySegment<Mod>.Empty;
|
||||
Mods = mods ?? Array.Empty<Mod>();
|
||||
ScoreProcessor = scoreProcessor ?? ruleset.CreateScoreProcessor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
@ -17,9 +18,7 @@ using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
@ -43,8 +42,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
[Resolved]
|
||||
private UserLookupCache userLookupCache { get; set; }
|
||||
|
||||
private readonly RulesetInfo ruleset;
|
||||
private readonly ScoreProcessor scoreProcessor;
|
||||
private readonly MultiplayerRoomUser[] playingUsers;
|
||||
private Bindable<ScoringMode> scoringMode;
|
||||
|
||||
@ -55,57 +52,56 @@ namespace osu.Game.Screens.Play.HUD
|
||||
/// <summary>
|
||||
/// Construct a new leaderboard.
|
||||
/// </summary>
|
||||
/// <param name="ruleset">The ruleset.</param>
|
||||
/// <param name="scoreProcessor">A score processor instance to handle score calculation for scores of users in the match.</param>
|
||||
/// <param name="users">IDs of all users in this match.</param>
|
||||
public MultiplayerGameplayLeaderboard(RulesetInfo ruleset, ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users)
|
||||
public MultiplayerGameplayLeaderboard(MultiplayerRoomUser[] users)
|
||||
{
|
||||
// todo: this will eventually need to be created per user to support different mod combinations.
|
||||
this.ruleset = ruleset;
|
||||
this.scoreProcessor = scoreProcessor;
|
||||
|
||||
playingUsers = users;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config, IAPIProvider api)
|
||||
private void load(OsuConfigManager config, IAPIProvider api, CancellationToken cancellationToken)
|
||||
{
|
||||
scoringMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
|
||||
|
||||
foreach (var user in playingUsers)
|
||||
{
|
||||
var trackedUser = CreateUserData(user, ruleset, scoreProcessor);
|
||||
|
||||
trackedUser.ScoringMode.BindTo(scoringMode);
|
||||
trackedUser.Score.BindValueChanged(_ => Scheduler.AddOnce(updateTotals));
|
||||
var scoreProcessor = new SpectatorScoreProcessor(user.UserID);
|
||||
scoreProcessor.Mode.BindTo(scoringMode);
|
||||
scoreProcessor.TotalScore.BindValueChanged(_ => Scheduler.AddOnce(updateTotals));
|
||||
AddInternal(scoreProcessor);
|
||||
|
||||
var trackedUser = new TrackedUserData(user, scoreProcessor);
|
||||
UserScores[user.UserID] = trackedUser;
|
||||
|
||||
if (trackedUser.Team is int team && !TeamScores.ContainsKey(team))
|
||||
TeamScores.Add(team, new BindableLong());
|
||||
}
|
||||
|
||||
userLookupCache.GetUsersAsync(playingUsers.Select(u => u.UserID).ToArray()).ContinueWith(task => Schedule(() =>
|
||||
{
|
||||
var users = task.GetResultSafely();
|
||||
userLookupCache.GetUsersAsync(playingUsers.Select(u => u.UserID).ToArray(), cancellationToken)
|
||||
.ContinueWith(task =>
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
var users = task.GetResultSafely();
|
||||
|
||||
for (int i = 0; i < users.Length; i++)
|
||||
{
|
||||
var user = users[i] ?? new APIUser
|
||||
{
|
||||
Id = playingUsers[i].UserID,
|
||||
Username = "Unknown user",
|
||||
};
|
||||
for (int i = 0; i < users.Length; i++)
|
||||
{
|
||||
var user = users[i] ?? new APIUser
|
||||
{
|
||||
Id = playingUsers[i].UserID,
|
||||
Username = "Unknown user",
|
||||
};
|
||||
|
||||
var trackedUser = UserScores[user.Id];
|
||||
var trackedUser = UserScores[user.Id];
|
||||
|
||||
var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id);
|
||||
leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy);
|
||||
leaderboardScore.TotalScore.BindTo(trackedUser.Score);
|
||||
leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo);
|
||||
leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit);
|
||||
}
|
||||
}));
|
||||
var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id);
|
||||
leaderboardScore.Accuracy.BindTo(trackedUser.ScoreProcessor.Accuracy);
|
||||
leaderboardScore.TotalScore.BindTo(trackedUser.ScoreProcessor.TotalScore);
|
||||
leaderboardScore.Combo.BindTo(trackedUser.ScoreProcessor.Combo);
|
||||
leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit);
|
||||
}
|
||||
});
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -118,20 +114,15 @@ namespace osu.Game.Screens.Play.HUD
|
||||
spectatorClient.WatchUser(user.UserID);
|
||||
|
||||
if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(user.UserID))
|
||||
usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { user.UserID }));
|
||||
playingUsersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { user.UserID }));
|
||||
}
|
||||
|
||||
// bind here is to support players leaving the match.
|
||||
// new players are not supported.
|
||||
playingUserIds.BindTo(multiplayerClient.CurrentMatchPlayingUserIds);
|
||||
playingUserIds.BindCollectionChanged(usersChanged);
|
||||
|
||||
// this leaderboard should be guaranteed to be completely loaded before the gameplay starts (is a prerequisite in MultiplayerPlayer).
|
||||
spectatorClient.OnNewFrames += handleIncomingFrames;
|
||||
playingUserIds.BindCollectionChanged(playingUsersChanged);
|
||||
}
|
||||
|
||||
protected virtual TrackedUserData CreateUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor) => new TrackedUserData(user, ruleset, scoreProcessor);
|
||||
|
||||
protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(APIUser user, bool isTracked)
|
||||
{
|
||||
var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked);
|
||||
@ -157,7 +148,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
}
|
||||
}
|
||||
|
||||
private void usersChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
private void playingUsersChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
@ -174,15 +165,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
}
|
||||
}
|
||||
|
||||
private void handleIncomingFrames(int userId, FrameDataBundle bundle) => Schedule(() =>
|
||||
{
|
||||
if (!UserScores.TryGetValue(userId, out var trackedData))
|
||||
return;
|
||||
|
||||
trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header));
|
||||
trackedData.UpdateScore();
|
||||
});
|
||||
|
||||
private void updateTotals()
|
||||
{
|
||||
if (!hasTeams)
|
||||
@ -196,7 +178,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
continue;
|
||||
|
||||
if (TeamScores.TryGetValue(u.Team.Value, out var team))
|
||||
team.Value += (int)Math.Round(u.Score.Value);
|
||||
team.Value += (int)Math.Round(u.ScoreProcessor.TotalScore.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,83 +189,26 @@ namespace osu.Game.Screens.Play.HUD
|
||||
if (spectatorClient != null)
|
||||
{
|
||||
foreach (var user in playingUsers)
|
||||
{
|
||||
spectatorClient.StopWatchingUser(user.UserID);
|
||||
}
|
||||
|
||||
spectatorClient.OnNewFrames -= handleIncomingFrames;
|
||||
}
|
||||
}
|
||||
|
||||
protected class TrackedUserData
|
||||
{
|
||||
public readonly MultiplayerRoomUser User;
|
||||
public readonly ScoreProcessor ScoreProcessor;
|
||||
public readonly SpectatorScoreProcessor ScoreProcessor;
|
||||
|
||||
public readonly BindableDouble Score = new BindableDouble();
|
||||
public readonly BindableDouble Accuracy = new BindableDouble(1);
|
||||
public readonly BindableInt CurrentCombo = new BindableInt();
|
||||
public readonly BindableBool UserQuit = new BindableBool();
|
||||
|
||||
public readonly IBindable<ScoringMode> ScoringMode = new Bindable<ScoringMode>();
|
||||
|
||||
public readonly List<TimedFrame> Frames = new List<TimedFrame>();
|
||||
|
||||
public int? Team => (User.MatchState as TeamVersusUserState)?.TeamID;
|
||||
|
||||
private readonly ScoreInfo scoreInfo;
|
||||
|
||||
public TrackedUserData(MultiplayerRoomUser user, RulesetInfo ruleset, ScoreProcessor scoreProcessor)
|
||||
public TrackedUserData(MultiplayerRoomUser user, SpectatorScoreProcessor scoreProcessor)
|
||||
{
|
||||
User = user;
|
||||
ScoreProcessor = scoreProcessor;
|
||||
|
||||
scoreInfo = new ScoreInfo { Ruleset = ruleset };
|
||||
|
||||
ScoringMode.BindValueChanged(_ => UpdateScore());
|
||||
}
|
||||
|
||||
public void MarkUserQuit() => UserQuit.Value = true;
|
||||
|
||||
public virtual void UpdateScore()
|
||||
{
|
||||
if (Frames.Count == 0)
|
||||
return;
|
||||
|
||||
SetFrame(Frames.Last());
|
||||
}
|
||||
|
||||
protected void SetFrame(TimedFrame frame)
|
||||
{
|
||||
var header = frame.Header;
|
||||
|
||||
scoreInfo.MaxCombo = header.MaxCombo;
|
||||
scoreInfo.Statistics = header.Statistics;
|
||||
|
||||
Score.Value = ScoreProcessor.ComputePartialScore(ScoringMode.Value, scoreInfo);
|
||||
|
||||
Accuracy.Value = header.Accuracy;
|
||||
CurrentCombo.Value = header.Combo;
|
||||
}
|
||||
}
|
||||
|
||||
protected class TimedFrame : IComparable<TimedFrame>
|
||||
{
|
||||
public readonly double Time;
|
||||
public readonly FrameHeader Header;
|
||||
|
||||
public TimedFrame(double time)
|
||||
{
|
||||
Time = time;
|
||||
}
|
||||
|
||||
public TimedFrame(double time, FrameHeader header)
|
||||
{
|
||||
Time = time;
|
||||
Header = header;
|
||||
}
|
||||
|
||||
public int CompareTo(TimedFrame other) => Time.CompareTo(other.Time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -213,8 +213,8 @@ namespace osu.Game.Screens.Play
|
||||
dependencies.CacheAs(DrawableRuleset);
|
||||
|
||||
ScoreProcessor = ruleset.CreateScoreProcessor();
|
||||
ScoreProcessor.ApplyBeatmap(playableBeatmap);
|
||||
ScoreProcessor.Mods.Value = gameplayMods;
|
||||
ScoreProcessor.ApplyBeatmap(playableBeatmap);
|
||||
|
||||
dependencies.CacheAs(ScoreProcessor);
|
||||
|
||||
@ -237,7 +237,7 @@ namespace osu.Game.Screens.Play
|
||||
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
|
||||
Score.ScoreInfo.Mods = gameplayMods;
|
||||
|
||||
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score));
|
||||
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor));
|
||||
|
||||
var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin);
|
||||
|
||||
|
@ -159,6 +159,8 @@ namespace osu.Game.Screens.Ranking.Expanded
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = beatmap.DifficultyName,
|
||||
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold),
|
||||
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
|
||||
Truncate = true,
|
||||
},
|
||||
new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12))
|
||||
{
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
@ -97,6 +98,8 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
protected readonly CarouselScrollContainer Scroll;
|
||||
|
||||
private readonly NoResultsPlaceholder noResultsPlaceholder;
|
||||
|
||||
private IEnumerable<CarouselBeatmapSet> beatmapSets => root.Children.OfType<CarouselBeatmapSet>();
|
||||
|
||||
// todo: only used for testing, maybe remove.
|
||||
@ -170,7 +173,8 @@ namespace osu.Game.Screens.Select
|
||||
Scroll = new CarouselScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
},
|
||||
noResultsPlaceholder = new NoResultsPlaceholder()
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -633,7 +637,7 @@ namespace osu.Game.Screens.Select
|
||||
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
|
||||
{
|
||||
// handles the vertical size of the carousel changing (ie. on window resize when aspect ratio has changed).
|
||||
if ((invalidation & Invalidation.Layout) > 0)
|
||||
if (invalidation.HasFlagFast(Invalidation.DrawSize))
|
||||
itemsCache.Invalidate();
|
||||
|
||||
return base.OnInvalidate(invalidation, source);
|
||||
@ -648,8 +652,18 @@ namespace osu.Game.Screens.Select
|
||||
// First we iterate over all non-filtered carousel items and populate their
|
||||
// vertical position data.
|
||||
if (revalidateItems)
|
||||
{
|
||||
updateYPositions();
|
||||
|
||||
if (visibleItems.Count == 0)
|
||||
{
|
||||
noResultsPlaceholder.Filter = activeCriteria;
|
||||
noResultsPlaceholder.Show();
|
||||
}
|
||||
else
|
||||
noResultsPlaceholder.Hide();
|
||||
}
|
||||
|
||||
// if there is a pending scroll action we apply it without animation and transfer the difference in position to the panels.
|
||||
// this is intentionally applied before updating the visible range below, to avoid animating new items (sourced from pool) from locations off-screen, as it looks bad.
|
||||
if (pendingScrollOperation != PendingScrollOperation.None)
|
||||
|
@ -1,33 +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;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public class ImportFromStablePopup : PopupDialog
|
||||
{
|
||||
public ImportFromStablePopup(Action importFromStable)
|
||||
{
|
||||
HeaderText = @"You have no beatmaps!";
|
||||
BodyText = "Would you like to import your beatmaps, skins, collections and scores from an existing osu!stable installation?\nThis will create a second copy of all files on disk.";
|
||||
|
||||
Icon = FontAwesome.Solid.Plane;
|
||||
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogOkButton
|
||||
{
|
||||
Text = @"Yes please!",
|
||||
Action = importFromStable
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = @"No, I'd like to start from scratch",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
145
osu.Game/Screens/Select/NoResultsPlaceholder.cs
Normal file
145
osu.Game/Screens/Select/NoResultsPlaceholder.cs
Normal file
@ -0,0 +1,145 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public class NoResultsPlaceholder : VisibilityContainer
|
||||
{
|
||||
private FilterCriteria? filter;
|
||||
|
||||
private LinkFlowContainer textFlow = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private FirstRunSetupOverlay? firstRunSetupOverlay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
public FilterCriteria Filter
|
||||
{
|
||||
set
|
||||
{
|
||||
if (filter == value)
|
||||
return;
|
||||
|
||||
filter = value;
|
||||
Scheduler.AddOnce(updateText);
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Masking = true;
|
||||
CornerRadius = 10;
|
||||
|
||||
Width = 300;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colours.Gray2,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new SpriteIcon
|
||||
{
|
||||
Icon = FontAwesome.Regular.SadTear,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Margin = new MarginPadding(10),
|
||||
Size = new Vector2(50),
|
||||
},
|
||||
textFlow = new LinkFlowContainer
|
||||
{
|
||||
Y = 60,
|
||||
Padding = new MarginPadding(10),
|
||||
TextAnchor = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn(600, Easing.OutQuint);
|
||||
|
||||
Scheduler.AddOnce(updateText);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.FadeOut(200, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void updateText()
|
||||
{
|
||||
// TODO: Refresh this text when new beatmaps are imported. Right now it won't get up-to-date suggestions.
|
||||
|
||||
// Bounce should play every time the filter criteria is updated.
|
||||
this.ScaleTo(0.9f)
|
||||
.ScaleTo(1f, 1000, Easing.OutElastic);
|
||||
|
||||
textFlow.Clear();
|
||||
|
||||
if (beatmaps.QueryBeatmapSet(s => !s.Protected && !s.DeletePending) == null)
|
||||
{
|
||||
textFlow.AddParagraph("No beatmaps found!");
|
||||
textFlow.AddParagraph(string.Empty);
|
||||
|
||||
textFlow.AddParagraph("Consider using the \"");
|
||||
textFlow.AddLink(FirstRunSetupOverlayStrings.FirstRunSetupTitle, () => firstRunSetupOverlay?.Show());
|
||||
textFlow.AddText("\" to download or import some beatmaps!");
|
||||
}
|
||||
else
|
||||
{
|
||||
textFlow.AddParagraph("No beatmaps match your filter criteria!");
|
||||
textFlow.AddParagraph(string.Empty);
|
||||
|
||||
if (string.IsNullOrEmpty(filter?.SearchText))
|
||||
{
|
||||
// TODO: Add realm queries to hint at which ruleset results are available in (and allow clicking to switch).
|
||||
// TODO: Make this message more certain by ensuring the osu! beatmaps exist before suggesting.
|
||||
if (filter?.Ruleset.OnlineID > 0 && !filter.AllowConvertedBeatmaps)
|
||||
{
|
||||
textFlow.AddParagraph("Beatmaps may be available by ");
|
||||
textFlow.AddLink("enabling automatic conversion", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
|
||||
textFlow.AddText("!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
textFlow.AddParagraph("You can try ");
|
||||
textFlow.AddLink("searching online", LinkAction.SearchBeatmapSet, filter.SearchText);
|
||||
textFlow.AddText(" for this query.");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add clickable link to reset criteria.
|
||||
}
|
||||
}
|
||||
}
|
42
osu.Game/Screens/Select/SkinDeleteDialog.cs
Normal file
42
osu.Game/Screens/Select/SkinDeleteDialog.cs
Normal file
@ -0,0 +1,42 @@
|
||||
// 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.Sprites;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public class SkinDeleteDialog : PopupDialog
|
||||
{
|
||||
[Resolved]
|
||||
private SkinManager manager { get; set; }
|
||||
|
||||
public SkinDeleteDialog(Skin skin)
|
||||
{
|
||||
BodyText = skin.SkinInfo.Value.Name;
|
||||
Icon = FontAwesome.Regular.TrashAlt;
|
||||
HeaderText = @"Confirm deletion of";
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogDangerousButton
|
||||
{
|
||||
Text = @"Yes. Totally. Delete it.",
|
||||
Action = () =>
|
||||
{
|
||||
if (manager == null)
|
||||
return;
|
||||
|
||||
manager.Delete(skin.SkinInfo.Value);
|
||||
manager.CurrentSkinInfo.SetDefault();
|
||||
},
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = @"Firetruck, I didn't mean to!",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -28,7 +28,6 @@ using osuTK.Input;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
@ -37,7 +36,6 @@ using osu.Game.Graphics.UserInterface;
|
||||
using System.Diagnostics;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
@ -59,8 +57,6 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
protected virtual bool ShowFooter => true;
|
||||
|
||||
protected virtual bool DisplayStableImportPrompt => legacyImportManager?.SupportsImportFromStable == true;
|
||||
|
||||
public override bool? AllowTrackAdjustments => true;
|
||||
|
||||
/// <summary>
|
||||
@ -94,14 +90,13 @@ namespace osu.Game.Screens.Select
|
||||
protected Container LeftArea { get; private set; }
|
||||
|
||||
private BeatmapInfoWedge beatmapInfoWedge;
|
||||
private IDialogOverlay dialogOverlay;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private IDialogOverlay dialogOverlay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private LegacyImportManager legacyImportManager { get; set; }
|
||||
|
||||
protected ModSelectOverlay ModSelect { get; private set; }
|
||||
|
||||
protected Sample SampleConfirm { get; private set; }
|
||||
@ -127,7 +122,7 @@ namespace osu.Game.Screens.Select
|
||||
internal IOverlayManager OverlayManager { get; private set; }
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(AudioManager audio, IDialogOverlay dialog, OsuColour colours, ManageCollectionsDialog manageCollectionsDialog, DifficultyRecommender recommender)
|
||||
private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog manageCollectionsDialog, DifficultyRecommender recommender)
|
||||
{
|
||||
// initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter).
|
||||
transferRulesetValue();
|
||||
@ -289,26 +284,9 @@ namespace osu.Game.Screens.Select
|
||||
BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo));
|
||||
}
|
||||
|
||||
dialogOverlay = dialog;
|
||||
|
||||
sampleChangeDifficulty = audio.Samples.Get(@"SongSelect/select-difficulty");
|
||||
sampleChangeBeatmap = audio.Samples.Get(@"SongSelect/select-expand");
|
||||
SampleConfirm = audio.Samples.Get(@"SongSelect/confirm-selection");
|
||||
|
||||
if (dialogOverlay != null)
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
// if we have no beatmaps, let's prompt the user to import from over a stable install if he has one.
|
||||
if (beatmaps.QueryBeatmapSet(s => !s.Protected && !s.DeletePending) == null && DisplayStableImportPrompt)
|
||||
{
|
||||
dialogOverlay.Push(new ImportFromStablePopup(() =>
|
||||
{
|
||||
Task.Run(() => legacyImportManager.ImportFromStableAsync(StableContent.All));
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
@ -29,6 +29,8 @@ namespace osu.Game.Skinning.Editor
|
||||
{
|
||||
public const double TRANSITION_DURATION = 500;
|
||||
|
||||
public const float MENU_HEIGHT = 40;
|
||||
|
||||
public readonly BindableList<ISkinnableDrawable> SelectedComponents = new BindableList<ISkinnableDrawable>();
|
||||
|
||||
protected override bool StartHidden => true;
|
||||
@ -78,8 +80,6 @@ namespace osu.Game.Skinning.Editor
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
const float menu_height = 40;
|
||||
|
||||
InternalChild = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -102,7 +102,7 @@ namespace osu.Game.Skinning.Editor
|
||||
Name = "Menu container",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Depth = float.MinValue,
|
||||
Height = menu_height,
|
||||
Height = MENU_HEIGHT,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new EditorMenuBar
|
||||
@ -322,7 +322,10 @@ namespace osu.Game.Skinning.Editor
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn(TRANSITION_DURATION, Easing.OutQuint);
|
||||
this
|
||||
// align animation to happen after the majority of the ScalingContainer animation completes.
|
||||
.Delay(ScalingContainer.TRANSITION_DURATION * 0.3f)
|
||||
.FadeIn(TRANSITION_DURATION, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
|
@ -12,6 +12,8 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Skinning.Editor
|
||||
{
|
||||
@ -33,6 +35,8 @@ namespace osu.Game.Skinning.Editor
|
||||
|
||||
private OsuScreen lastTargetScreen;
|
||||
|
||||
private Vector2 lastDrawSize;
|
||||
|
||||
public SkinEditorOverlay(ScalingContainer scalingContainer)
|
||||
{
|
||||
this.scalingContainer = scalingContainer;
|
||||
@ -81,15 +85,42 @@ namespace osu.Game.Skinning.Editor
|
||||
|
||||
protected override void PopOut() => skinEditor?.Hide();
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (game.DrawSize != lastDrawSize)
|
||||
{
|
||||
lastDrawSize = game.DrawSize;
|
||||
updateScreenSizing();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateScreenSizing()
|
||||
{
|
||||
if (skinEditor?.State.Value != Visibility.Visible) return;
|
||||
|
||||
const float padding = 10;
|
||||
|
||||
float relativeSidebarWidth = (EditorSidebar.WIDTH + padding) / DrawWidth;
|
||||
float relativeToolbarHeight = (SkinEditorSceneLibrary.HEIGHT + SkinEditor.MENU_HEIGHT + padding) / DrawHeight;
|
||||
|
||||
var rect = new RectangleF(
|
||||
relativeSidebarWidth,
|
||||
relativeToolbarHeight,
|
||||
1 - relativeSidebarWidth * 2,
|
||||
1f - relativeToolbarHeight - padding / DrawHeight);
|
||||
|
||||
scalingContainer.SetCustomRect(rect, true);
|
||||
}
|
||||
|
||||
private void updateComponentVisibility()
|
||||
{
|
||||
Debug.Assert(skinEditor != null);
|
||||
|
||||
const float toolbar_padding_requirement = 0.18f;
|
||||
|
||||
if (skinEditor.State.Value == Visibility.Visible)
|
||||
{
|
||||
scalingContainer.SetCustomRect(new RectangleF(toolbar_padding_requirement, 0.2f, 0.8f - toolbar_padding_requirement, 0.7f), true);
|
||||
Scheduler.AddOnce(updateScreenSizing);
|
||||
|
||||
game?.Toolbar.Hide();
|
||||
game?.CloseAllOverlays();
|
||||
@ -127,6 +158,9 @@ namespace osu.Game.Skinning.Editor
|
||||
|
||||
private void setTarget(OsuScreen target)
|
||||
{
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
Debug.Assert(skinEditor != null);
|
||||
|
||||
if (!target.IsLoaded)
|
||||
|
@ -27,6 +27,8 @@ namespace osu.Game.Skinning.Editor
|
||||
{
|
||||
public class SkinEditorSceneLibrary : CompositeDrawable
|
||||
{
|
||||
public const float HEIGHT = BUTTON_HEIGHT + padding * 2;
|
||||
|
||||
public const float BUTTON_HEIGHT = 40;
|
||||
|
||||
private const float padding = 10;
|
||||
@ -42,7 +44,7 @@ namespace osu.Game.Skinning.Editor
|
||||
|
||||
public SkinEditorSceneLibrary()
|
||||
{
|
||||
Height = BUTTON_HEIGHT + padding * 2;
|
||||
Height = HEIGHT;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -303,8 +303,13 @@ namespace osu.Game.Skinning
|
||||
if (Configuration.ConfigDictionary.TryGetValue(lookup.ToString(), out string val))
|
||||
{
|
||||
// special case for handling skins which use 1 or 0 to signify a boolean state.
|
||||
// ..or in some cases 2 (https://github.com/ppy/osu/issues/18579).
|
||||
if (typeof(TValue) == typeof(bool))
|
||||
val = val == "1" ? "true" : "false";
|
||||
{
|
||||
val = bool.TryParse(val, out bool boolVal)
|
||||
? Convert.ChangeType(boolVal, typeof(bool)).ToString()
|
||||
: Convert.ChangeType(Convert.ToInt32(val), typeof(bool)).ToString();
|
||||
}
|
||||
|
||||
var bindable = new Bindable<TValue>();
|
||||
if (val != null)
|
||||
|
@ -26,7 +26,10 @@ namespace osu.Game.Tests.Gameplay
|
||||
var workingBeatmap = new TestWorkingBeatmap(beatmap);
|
||||
var playableBeatmap = workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods);
|
||||
|
||||
return new GameplayState(playableBeatmap, ruleset, mods, score);
|
||||
var scoreProcessor = ruleset.CreateScoreProcessor();
|
||||
scoreProcessor.ApplyBeatmap(playableBeatmap);
|
||||
|
||||
return new GameplayState(playableBeatmap, ruleset, mods, score, scoreProcessor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Toolkit.HighPerformance" Version="7.1.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="ppy.LocalisationAnalyser" Version="2022.417.0">
|
||||
<PackageReference Include="ppy.LocalisationAnalyser" Version="2022.607.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
@ -790,6 +790,15 @@ See the LICENCE file in the repository root for full licence text.
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FFIELD/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FRESOURCE/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CustomTools/CustomToolsData/@EntryValue"></s:String>
|
||||
<s:Boolean x:Key="/Default/Environment/AutoImport2/=CSHARP/BlackLists/=Microsoft_002EExtensions_002ELogging_002E_002A/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/AutoImport2/=CSHARP/BlackLists/=Microsoft_002EToolkit_002EHighPeformance_002EBox_002A/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/AutoImport2/=CSHARP/BlackLists/=NUnit_002EFramework_002EInternal_002ELogger/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/AutoImport2/=CSHARP/BlackLists/=OpenTabletDriver_002EPlugin_002EDependencyInjection_002E_002A/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/AutoImport2/=CSHARP/BlackLists/=Realms_002ELogging_002ELogger/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/AutoImport2/=CSHARP/BlackLists/=System_002EComponentModel_002EContainer/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/AutoImport2/=CSHARP/BlackLists/=System_002ENumerics_002E_002A/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/AutoImport2/=CSHARP/BlackLists/=System_002ESecurity_002ECryptography_002ERSA/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/AutoImport2/=CSHARP/BlackLists/=TagLib_002EMpeg4_002EBox/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002EDaemon_002ESettings_002EMigration_002ESwaWarningsModeSettingsMigrate/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
|
Loading…
x
Reference in New Issue
Block a user