mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 11:37:28 +08:00
Merge branch 'master' into beatmap-difficulty-more-interface-usage
This commit is contained in:
commit
e837a3511d
@ -52,7 +52,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.929.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1004.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
|
@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
Mods = mods,
|
||||
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
||||
GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),
|
||||
ScoreMultiplier = getScoreMultiplier(beatmap, mods),
|
||||
ScoreMultiplier = getScoreMultiplier(mods),
|
||||
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
|
||||
Skills = skills
|
||||
};
|
||||
@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
|
||||
{
|
||||
new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns)
|
||||
new Strain(mods, ((ManiaBeatmap)Beatmap).TotalColumns)
|
||||
};
|
||||
|
||||
protected override Mod[] DifficultyAdjustmentMods
|
||||
@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
}
|
||||
}
|
||||
|
||||
private double getScoreMultiplier(IBeatmap beatmap, Mod[] mods)
|
||||
private double getScoreMultiplier(Mod[] mods)
|
||||
{
|
||||
double scoreMultiplier = 1;
|
||||
|
||||
@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
}
|
||||
}
|
||||
|
||||
var maniaBeatmap = (ManiaBeatmap)beatmap;
|
||||
var maniaBeatmap = (ManiaBeatmap)Beatmap;
|
||||
int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns;
|
||||
|
||||
if (diff > 0)
|
||||
|
@ -909,7 +909,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
|
||||
var manager = osu.Dependencies.Get<BeatmapManager>();
|
||||
|
||||
var importedSet = await manager.Import(new ImportTask(temp));
|
||||
var importedSet = await manager.Import(new ImportTask(temp)).ConfigureAwait(false);
|
||||
|
||||
ensureLoaded(osu);
|
||||
|
||||
@ -924,7 +924,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
|
||||
var manager = osu.Dependencies.Get<BeatmapManager>();
|
||||
|
||||
var importedSet = await manager.Import(new ImportTask(temp));
|
||||
var importedSet = await manager.Import(new ImportTask(temp)).ConfigureAwait(false);
|
||||
|
||||
ensureLoaded(osu);
|
||||
|
||||
|
@ -42,7 +42,7 @@ namespace osu.Game.Tests.Online
|
||||
|
||||
AddAssert("response event fired", () => response != null);
|
||||
|
||||
AddAssert("request has response", () => request.Result == response);
|
||||
AddAssert("request has response", () => request.Response == response);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -42,6 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public void TestEmptyLegacyBeatmapSkinFallsBack()
|
||||
{
|
||||
CreateSkinTest(SkinInfo.Default, () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null));
|
||||
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
|
||||
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value));
|
||||
}
|
||||
|
||||
@ -84,18 +85,18 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
Remove(expectedComponentsAdjustmentContainer);
|
||||
|
||||
return almostEqual(actualInfo, expectedInfo);
|
||||
|
||||
static bool almostEqual(SkinnableInfo info, SkinnableInfo other) =>
|
||||
other != null
|
||||
&& info.Type == other.Type
|
||||
&& info.Anchor == other.Anchor
|
||||
&& info.Origin == other.Origin
|
||||
&& Precision.AlmostEquals(info.Position, other.Position)
|
||||
&& Precision.AlmostEquals(info.Scale, other.Scale)
|
||||
&& Precision.AlmostEquals(info.Rotation, other.Rotation)
|
||||
&& info.Children.SequenceEqual(other.Children, new FuncEqualityComparer<SkinnableInfo>(almostEqual));
|
||||
}
|
||||
|
||||
private static bool almostEqual(SkinnableInfo info, SkinnableInfo other) =>
|
||||
other != null
|
||||
&& info.Type == other.Type
|
||||
&& info.Anchor == other.Anchor
|
||||
&& info.Origin == other.Origin
|
||||
&& Precision.AlmostEquals(info.Position, other.Position, 1)
|
||||
&& Precision.AlmostEquals(info.Scale, other.Scale)
|
||||
&& Precision.AlmostEquals(info.Rotation, other.Rotation)
|
||||
&& info.Children.SequenceEqual(other.Children, new FuncEqualityComparer<SkinnableInfo>(almostEqual));
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
||||
=> new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, Audio, currentBeatmapSkin);
|
||||
|
||||
|
@ -0,0 +1,108 @@
|
||||
// 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.Diagnostics;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public class TestScenePerformancePointsCounter : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private GameplayState gameplayState;
|
||||
|
||||
[Cached]
|
||||
private ScoreProcessor scoreProcessor;
|
||||
|
||||
private int iteration;
|
||||
private PerformancePointsCounter counter;
|
||||
|
||||
public TestScenePerformancePointsCounter()
|
||||
{
|
||||
var ruleset = CreateRuleset();
|
||||
|
||||
Debug.Assert(ruleset != null);
|
||||
|
||||
var beatmap = CreateWorkingBeatmap(ruleset.RulesetInfo)
|
||||
.GetPlayableBeatmap(ruleset.RulesetInfo);
|
||||
|
||||
gameplayState = new GameplayState(beatmap, ruleset);
|
||||
scoreProcessor = new ScoreProcessor();
|
||||
}
|
||||
|
||||
protected override Ruleset CreateRuleset() => new OsuRuleset();
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("Create counter", () =>
|
||||
{
|
||||
iteration = 0;
|
||||
|
||||
Child = counter = new PerformancePointsCounter
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(5),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicCounting()
|
||||
{
|
||||
int previousValue = 0;
|
||||
|
||||
AddAssert("counter displaying zero", () => counter.Current.Value == 0);
|
||||
|
||||
AddRepeatStep("Add judgement", applyOneJudgement, 10);
|
||||
|
||||
AddUntilStep("counter non-zero", () => counter.Current.Value > 0);
|
||||
AddUntilStep("counter opaque", () => counter.Child.Alpha == 1);
|
||||
|
||||
AddStep("Revert judgement", () =>
|
||||
{
|
||||
previousValue = counter.Current.Value;
|
||||
|
||||
scoreProcessor.RevertResult(new JudgementResult(new HitObject(), new OsuJudgement()));
|
||||
});
|
||||
|
||||
AddUntilStep("counter decreased", () => counter.Current.Value < previousValue);
|
||||
|
||||
AddStep("Add judgement", applyOneJudgement);
|
||||
|
||||
AddUntilStep("counter non-zero", () => counter.Current.Value > 0);
|
||||
}
|
||||
|
||||
private void applyOneJudgement()
|
||||
{
|
||||
var scoreInfo = gameplayState.Score.ScoreInfo;
|
||||
|
||||
scoreInfo.MaxCombo = iteration * 1000;
|
||||
scoreInfo.Accuracy = 1;
|
||||
scoreInfo.Statistics[HitResult.Great] = iteration * 1000;
|
||||
|
||||
scoreProcessor.ApplyResult(new OsuJudgementResult(new HitObject
|
||||
{
|
||||
StartTime = iteration * 10000,
|
||||
}, new OsuJudgement())
|
||||
{
|
||||
Type = HitResult.Perfect,
|
||||
});
|
||||
|
||||
iteration++;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online;
|
||||
@ -8,34 +9,29 @@ using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneReplayDownloadButton : OsuTestScene
|
||||
public class TestSceneReplayDownloadButton : OsuManualInputManagerTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; }
|
||||
|
||||
private TestReplayDownloadButton downloadButton;
|
||||
|
||||
public TestSceneReplayDownloadButton()
|
||||
[Test]
|
||||
public void TestDisplayStates()
|
||||
{
|
||||
createButton(true);
|
||||
AddStep(@"downloading state", () => downloadButton.SetDownloadState(DownloadState.Downloading));
|
||||
AddStep(@"locally available state", () => downloadButton.SetDownloadState(DownloadState.LocallyAvailable));
|
||||
AddStep(@"not downloaded state", () => downloadButton.SetDownloadState(DownloadState.NotDownloaded));
|
||||
createButton(false);
|
||||
createButtonNoScore();
|
||||
}
|
||||
|
||||
private void createButton(bool withReplay)
|
||||
{
|
||||
AddStep(withReplay ? @"create button with replay" : "create button without replay", () =>
|
||||
AddStep(@"create button with replay", () =>
|
||||
{
|
||||
Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(withReplay))
|
||||
Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(true))
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -43,9 +39,81 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
|
||||
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
|
||||
|
||||
AddStep(@"downloading state", () => downloadButton.SetDownloadState(DownloadState.Downloading));
|
||||
AddStep(@"locally available state", () => downloadButton.SetDownloadState(DownloadState.LocallyAvailable));
|
||||
AddStep(@"not downloaded state", () => downloadButton.SetDownloadState(DownloadState.NotDownloaded));
|
||||
}
|
||||
|
||||
private void createButtonNoScore()
|
||||
[Test]
|
||||
public void TestButtonWithReplayStartsDownload()
|
||||
{
|
||||
bool downloadStarted = false;
|
||||
bool downloadFinished = false;
|
||||
|
||||
AddStep(@"create button with replay", () =>
|
||||
{
|
||||
downloadStarted = false;
|
||||
downloadFinished = false;
|
||||
|
||||
Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(true))
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
|
||||
downloadButton.State.BindValueChanged(state =>
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case DownloadState.Downloading:
|
||||
downloadStarted = true;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (state.OldValue)
|
||||
{
|
||||
case DownloadState.Downloading:
|
||||
downloadFinished = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
|
||||
|
||||
AddAssert("state is available", () => downloadButton.State.Value == DownloadState.NotDownloaded);
|
||||
|
||||
AddStep("click button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(downloadButton);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("state entered downloading", () => downloadStarted);
|
||||
AddUntilStep("state left downloading", () => downloadFinished);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestButtonWithoutReplay()
|
||||
{
|
||||
AddStep("create button without replay", () =>
|
||||
{
|
||||
Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(false))
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
});
|
||||
|
||||
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
|
||||
|
||||
AddAssert("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
|
||||
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateButtonWithNoScore()
|
||||
{
|
||||
AddStep("create button with null score", () =>
|
||||
{
|
||||
@ -57,6 +125,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
|
||||
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
|
||||
|
||||
AddAssert("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
|
||||
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
|
||||
}
|
||||
|
||||
private ScoreInfo getScoreInfo(bool replayAvailable)
|
||||
@ -78,6 +149,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public void SetDownloadState(DownloadState state) => State.Value = state;
|
||||
|
||||
public new Bindable<DownloadState> State => base.State;
|
||||
|
||||
public TestReplayDownloadButton(ScoreInfo score)
|
||||
: base(score)
|
||||
{
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -68,6 +69,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
LoadScreen(dependenciesScreen = new DependenciesScreen(client));
|
||||
});
|
||||
|
||||
AddUntilStep("wait for dependencies screen", () => Stack.CurrentScreen is DependenciesScreen);
|
||||
AddUntilStep("wait for dependencies to start load", () => dependenciesScreen.LoadState > LoadState.NotLoaded);
|
||||
AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded);
|
||||
|
||||
AddStep("load multiplayer", () => LoadScreen(multiplayerScreen));
|
||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
@ -123,6 +124,13 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddAssert("ruleset unchanged", () => ReferenceEquals(Ruleset.Value, ruleset));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSwitchThreadExecutionMode()
|
||||
{
|
||||
AddStep("Change thread mode to multi threaded", () => { game.Dependencies.Get<FrameworkConfigManager>().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.MultiThreaded); });
|
||||
AddStep("Change thread mode to single thread", () => { game.Dependencies.Get<FrameworkConfigManager>().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.SingleThread); });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUnavailableRulesetHandled()
|
||||
{
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -18,6 +19,7 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Chat;
|
||||
using osu.Game.Overlays.Chat.Selection;
|
||||
using osu.Game.Overlays.Chat.Tabs;
|
||||
using osu.Game.Users;
|
||||
@ -41,6 +43,9 @@ namespace osu.Game.Tests.Visual.Online
|
||||
private Channel channel2 => channels[1];
|
||||
private Channel channel3 => channels[2];
|
||||
|
||||
[CanBeNull]
|
||||
private Func<Channel, List<Message>> onGetMessages;
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
|
||||
@ -79,6 +84,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
AddStep("register request handling", () =>
|
||||
{
|
||||
onGetMessages = null;
|
||||
|
||||
((DummyAPIAccess)API).HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
@ -102,6 +109,12 @@ namespace osu.Game.Tests.Visual.Online
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
case GetMessagesRequest getMessages:
|
||||
var messages = onGetMessages?.Invoke(getMessages.Channel);
|
||||
if (messages != null)
|
||||
getMessages.TriggerSuccess(messages);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -122,14 +135,37 @@ namespace osu.Game.Tests.Visual.Online
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectingChannelClosesSelector()
|
||||
public void TestChannelSelection()
|
||||
{
|
||||
AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
|
||||
AddStep("Setup get message response", () => onGetMessages = channel =>
|
||||
{
|
||||
if (channel == channel1)
|
||||
{
|
||||
return new List<Message>
|
||||
{
|
||||
new Message(1)
|
||||
{
|
||||
ChannelId = channel1.Id,
|
||||
Content = "hello from channel 1!",
|
||||
Sender = new User
|
||||
{
|
||||
Id = 2,
|
||||
Username = "test_user"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
|
||||
AddStep("Switch to channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
|
||||
|
||||
AddAssert("Current channel is channel 1", () => currentChannel == channel1);
|
||||
AddUntilStep("Loading spinner hidden", () => chatOverlay.ChildrenOfType<LoadingSpinner>().All(spinner => !spinner.IsPresent));
|
||||
AddAssert("Channel message shown", () => chatOverlay.ChildrenOfType<ChatLine>().Count() == 1);
|
||||
AddAssert("Channel selector was closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden);
|
||||
}
|
||||
|
||||
|
@ -714,10 +714,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
});
|
||||
|
||||
FilterableDifficultyIcon difficultyIcon = null;
|
||||
AddStep("Find an icon for different ruleset", () =>
|
||||
AddUntilStep("Find an icon for different ruleset", () =>
|
||||
{
|
||||
difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>()
|
||||
.First(icon => icon.Item.BeatmapInfo.Ruleset.ID == 3);
|
||||
.FirstOrDefault(icon => icon.Item.BeatmapInfo.Ruleset.ID == 3);
|
||||
return difficultyIcon != null;
|
||||
});
|
||||
|
||||
AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0);
|
||||
|
@ -162,6 +162,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
|
||||
|
||||
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID));
|
||||
}
|
||||
|
||||
|
@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
var req = new GetBeatmapSetRequest(1);
|
||||
api.Queue(req);
|
||||
|
||||
AddUntilStep("wait for api response", () => req.Result != null);
|
||||
AddUntilStep("wait for api response", () => req.Response != null);
|
||||
|
||||
TestUpdateableBeatmapBackgroundSprite background = null;
|
||||
|
||||
@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Child = background = new TestUpdateableBeatmapBackgroundSprite
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Beatmap = { Value = new BeatmapInfo { BeatmapSet = req.Result?.ToBeatmapSet(rulesets) } }
|
||||
Beatmap = { Value = new BeatmapInfo { BeatmapSet = req.Response?.ToBeatmapSet(rulesets) } }
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -182,7 +182,7 @@ namespace osu.Game.Tournament
|
||||
{
|
||||
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID });
|
||||
API.Perform(req);
|
||||
b.BeatmapInfo = req.Result?.ToBeatmapInfo(RulesetStore);
|
||||
b.BeatmapInfo = req.Response?.ToBeatmapInfo(RulesetStore);
|
||||
|
||||
addedInfo = true;
|
||||
}
|
||||
@ -203,7 +203,7 @@ namespace osu.Game.Tournament
|
||||
{
|
||||
var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID });
|
||||
req.Perform(API);
|
||||
b.BeatmapInfo = req.Result?.ToBeatmapInfo(RulesetStore);
|
||||
b.BeatmapInfo = req.Response?.ToBeatmapInfo(RulesetStore);
|
||||
|
||||
addedInfo = true;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
@ -147,6 +148,14 @@ namespace osu.Game.Beatmaps
|
||||
}, token, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
|
||||
}
|
||||
|
||||
public Task<List<TimedDifficultyAttributes>> GetTimedDifficultyAttributesAsync(WorkingBeatmap beatmap, Ruleset ruleset, Mod[] mods, CancellationToken token = default)
|
||||
{
|
||||
return Task.Factory.StartNew(() => ruleset.CreateDifficultyCalculator(beatmap).CalculateTimed(mods),
|
||||
token,
|
||||
TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously,
|
||||
updateScheduler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="DifficultyRating"/> that describes a star rating.
|
||||
/// </summary>
|
||||
|
@ -78,7 +78,7 @@ namespace osu.Game.Beatmaps
|
||||
// intentionally blocking to limit web request concurrency
|
||||
api.Perform(req);
|
||||
|
||||
var res = req.Result;
|
||||
var res = req.Response;
|
||||
|
||||
if (res != null)
|
||||
{
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// The beatmap set this beatmap is part of.
|
||||
/// </summary>
|
||||
IBeatmapSetInfo BeatmapSet { get; }
|
||||
IBeatmapSetInfo? BeatmapSet { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The playable length in milliseconds of this beatmap.
|
||||
|
@ -81,7 +81,7 @@ namespace osu.Game.Beatmaps
|
||||
/// <returns>The applicable <see cref="IBeatmapConverter"/>.</returns>
|
||||
protected virtual IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) => ruleset.CreateBeatmapConverter(beatmap);
|
||||
|
||||
public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods = null, TimeSpan? timeout = null)
|
||||
public virtual IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods = null, TimeSpan? timeout = null)
|
||||
{
|
||||
using (var cancellationSource = createCancellationTokenSource(timeout))
|
||||
{
|
||||
|
@ -197,7 +197,7 @@ namespace osu.Game.Database
|
||||
else
|
||||
{
|
||||
notification.CompletionText = imported.Count == 1
|
||||
? $"Imported {imported.First()}!"
|
||||
? $"Imported {imported.First().Value}!"
|
||||
: $"Imported {imported.Count} {HumanisedModelName}s!";
|
||||
|
||||
if (imported.Count > 0 && PostImport != null)
|
||||
|
@ -115,7 +115,7 @@ namespace osu.Game.Database
|
||||
createNewTask();
|
||||
}
|
||||
|
||||
List<User> foundUsers = request.Result?.Users;
|
||||
List<User> foundUsers = request.Response?.Users;
|
||||
|
||||
if (foundUsers != null)
|
||||
{
|
||||
|
@ -25,7 +25,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
private SpriteText displayedCountSpriteText;
|
||||
private IHasText displayedCountText;
|
||||
|
||||
public Drawable DrawableCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the roll-up duration will be proportional to change in value.
|
||||
@ -72,16 +74,16 @@ namespace osu.Game.Graphics.UserInterface
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
displayedCountSpriteText = CreateSpriteText();
|
||||
displayedCountText = CreateText();
|
||||
|
||||
UpdateDisplay();
|
||||
Child = displayedCountSpriteText;
|
||||
Child = DrawableCount = (Drawable)displayedCountText;
|
||||
}
|
||||
|
||||
protected void UpdateDisplay()
|
||||
{
|
||||
if (displayedCountSpriteText != null)
|
||||
displayedCountSpriteText.Text = FormatCount(DisplayedCount);
|
||||
if (displayedCountText != null)
|
||||
displayedCountText.Text = FormatCount(DisplayedCount);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -160,6 +162,15 @@ namespace osu.Game.Graphics.UserInterface
|
||||
this.TransformTo(nameof(DisplayedCount), newValue, rollingTotalDuration, RollingEasing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the text. Delegates to <see cref="CreateSpriteText"/> by default.
|
||||
/// </summary>
|
||||
protected virtual IHasText CreateText() => CreateSpriteText();
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="OsuSpriteText"/> which may be used to display this counter's text.
|
||||
/// May not be called if <see cref="CreateText"/> is overridden.
|
||||
/// </summary>
|
||||
protected virtual OsuSpriteText CreateSpriteText() => new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Numeric.With(size: 40f),
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Framework.Logging;
|
||||
@ -17,7 +18,11 @@ namespace osu.Game.Online.API
|
||||
{
|
||||
protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest<T>(Uri);
|
||||
|
||||
public T Result { get; private set; }
|
||||
/// <summary>
|
||||
/// The deserialised response object. May be null if the request or deserialisation failed.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public T Response { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked on successful completion of an API request.
|
||||
@ -27,21 +32,21 @@ namespace osu.Game.Online.API
|
||||
|
||||
protected APIRequest()
|
||||
{
|
||||
base.Success += () => Success?.Invoke(Result);
|
||||
base.Success += () => Success?.Invoke(Response);
|
||||
}
|
||||
|
||||
protected override void PostProcess()
|
||||
{
|
||||
base.PostProcess();
|
||||
Result = ((OsuJsonWebRequest<T>)WebRequest)?.ResponseObject;
|
||||
Response = ((OsuJsonWebRequest<T>)WebRequest)?.ResponseObject;
|
||||
}
|
||||
|
||||
internal void TriggerSuccess(T result)
|
||||
{
|
||||
if (Result != null)
|
||||
if (Response != null)
|
||||
throw new InvalidOperationException("Attempted to trigger success more than once");
|
||||
|
||||
Result = result;
|
||||
Response = result;
|
||||
|
||||
TriggerSuccess();
|
||||
}
|
||||
|
@ -8,13 +8,13 @@ namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class GetMessagesRequest : APIRequest<List<Message>>
|
||||
{
|
||||
private readonly Channel channel;
|
||||
public readonly Channel Channel;
|
||||
|
||||
public GetMessagesRequest(Channel channel)
|
||||
{
|
||||
this.channel = channel;
|
||||
Channel = channel;
|
||||
}
|
||||
|
||||
protected override string Target => $@"chat/channels/{channel.Id}/messages";
|
||||
protected override string Target => $@"chat/channels/{Channel.Id}/messages";
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
[JsonProperty("count")]
|
||||
public int PlayCount { get; set; }
|
||||
|
||||
[JsonProperty]
|
||||
[JsonProperty("beatmap")]
|
||||
private BeatmapInfo beatmapInfo { get; set; }
|
||||
|
||||
[JsonProperty]
|
||||
|
@ -13,24 +13,23 @@ using osu.Framework.Development;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Input;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Spectator;
|
||||
@ -160,8 +159,6 @@ namespace osu.Game
|
||||
|
||||
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(GLOBAL_TRACK_VOLUME_ADJUST);
|
||||
|
||||
private IBindable<GameThreadState> updateThreadState;
|
||||
|
||||
public OsuGameBase()
|
||||
{
|
||||
UseDevelopmentServer = DebugUtils.IsDebugBuild;
|
||||
@ -189,9 +186,6 @@ namespace osu.Game
|
||||
|
||||
dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client"));
|
||||
|
||||
updateThreadState = Host.UpdateThread.State.GetBoundCopy();
|
||||
updateThreadState.BindValueChanged(updateThreadStateChanged);
|
||||
|
||||
AddInternal(realmFactory);
|
||||
|
||||
dependencies.CacheAs(Storage);
|
||||
@ -372,23 +366,6 @@ namespace osu.Game
|
||||
AddFont(Resources, @"Fonts/Venera/Venera-Black");
|
||||
}
|
||||
|
||||
private IDisposable blocking;
|
||||
|
||||
private void updateThreadStateChanged(ValueChangedEvent<GameThreadState> state)
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case GameThreadState.Running:
|
||||
blocking?.Dispose();
|
||||
blocking = null;
|
||||
break;
|
||||
|
||||
case GameThreadState.Paused:
|
||||
blocking = realmFactory.BlockAllOperations();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
@ -285,7 +285,7 @@ namespace osu.Game.Overlays
|
||||
return;
|
||||
|
||||
// check once more to ensure the channel hasn't since been removed from the loaded channels list (may have been left by some automated means).
|
||||
if (loadedChannels.Contains(loaded))
|
||||
if (!loadedChannels.Contains(loaded))
|
||||
return;
|
||||
|
||||
loading.Hide();
|
||||
|
@ -143,19 +143,27 @@ namespace osu.Game.Overlays
|
||||
switch (request)
|
||||
{
|
||||
case GetUserRankingsRequest userRequest:
|
||||
if (userRequest.Response == null)
|
||||
return null;
|
||||
|
||||
switch (userRequest.Type)
|
||||
{
|
||||
case UserRankingsType.Performance:
|
||||
return new PerformanceTable(1, userRequest.Result.Users);
|
||||
return new PerformanceTable(1, userRequest.Response.Users);
|
||||
|
||||
case UserRankingsType.Score:
|
||||
return new ScoresTable(1, userRequest.Result.Users);
|
||||
return new ScoresTable(1, userRequest.Response.Users);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
case GetCountryRankingsRequest countryRequest:
|
||||
return new CountriesTable(1, countryRequest.Result.Countries);
|
||||
{
|
||||
if (countryRequest.Response == null)
|
||||
return null;
|
||||
|
||||
return new CountriesTable(1, countryRequest.Response.Countries);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -7,6 +7,8 @@ using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -16,6 +18,14 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
{
|
||||
public abstract class DifficultyCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// The beatmap for which difficulty will be calculated.
|
||||
/// </summary>
|
||||
protected IBeatmap Beatmap { get; private set; }
|
||||
|
||||
private Mod[] playableMods;
|
||||
private double clockRate;
|
||||
|
||||
private readonly Ruleset ruleset;
|
||||
private readonly WorkingBeatmap beatmap;
|
||||
|
||||
@ -32,14 +42,45 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
/// <returns>A structure describing the difficulty of the beatmap.</returns>
|
||||
public DifficultyAttributes Calculate(params Mod[] mods)
|
||||
{
|
||||
mods = mods.Select(m => m.DeepClone()).ToArray();
|
||||
preProcess(mods);
|
||||
|
||||
IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods);
|
||||
var skills = CreateSkills(Beatmap, playableMods, clockRate);
|
||||
|
||||
var track = new TrackVirtual(10000);
|
||||
mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
if (!Beatmap.HitObjects.Any())
|
||||
return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
|
||||
|
||||
return calculate(playableBeatmap, mods, track.Rate);
|
||||
foreach (var hitObject in getDifficultyHitObjects())
|
||||
{
|
||||
foreach (var skill in skills)
|
||||
skill.ProcessInternal(hitObject);
|
||||
}
|
||||
|
||||
return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
|
||||
}
|
||||
|
||||
public List<TimedDifficultyAttributes> CalculateTimed(params Mod[] mods)
|
||||
{
|
||||
preProcess(mods);
|
||||
|
||||
var attribs = new List<TimedDifficultyAttributes>();
|
||||
|
||||
if (!Beatmap.HitObjects.Any())
|
||||
return attribs;
|
||||
|
||||
var skills = CreateSkills(Beatmap, playableMods, clockRate);
|
||||
var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap);
|
||||
|
||||
foreach (var hitObject in getDifficultyHitObjects())
|
||||
{
|
||||
progressiveBeatmap.HitObjects.Add(hitObject.BaseObject);
|
||||
|
||||
foreach (var skill in skills)
|
||||
skill.ProcessInternal(hitObject);
|
||||
|
||||
attribs.Add(new TimedDifficultyAttributes(hitObject.EndTime, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate)));
|
||||
}
|
||||
|
||||
return attribs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -57,24 +98,23 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
}
|
||||
}
|
||||
|
||||
private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="DifficultyHitObject"/>s to calculate against.
|
||||
/// </summary>
|
||||
private IEnumerable<DifficultyHitObject> getDifficultyHitObjects() => SortObjects(CreateDifficultyHitObjects(Beatmap, clockRate));
|
||||
|
||||
/// <summary>
|
||||
/// Performs required tasks before every calculation.
|
||||
/// </summary>
|
||||
/// <param name="mods">The original list of <see cref="Mod"/>s.</param>
|
||||
private void preProcess(Mod[] mods)
|
||||
{
|
||||
var skills = CreateSkills(beatmap, mods, clockRate);
|
||||
playableMods = mods.Select(m => m.DeepClone()).ToArray();
|
||||
Beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods);
|
||||
|
||||
if (!beatmap.HitObjects.Any())
|
||||
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
|
||||
|
||||
var difficultyHitObjects = SortObjects(CreateDifficultyHitObjects(beatmap, clockRate)).ToList();
|
||||
|
||||
foreach (var hitObject in difficultyHitObjects)
|
||||
{
|
||||
foreach (var skill in skills)
|
||||
{
|
||||
skill.ProcessInternal(hitObject);
|
||||
}
|
||||
}
|
||||
|
||||
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
|
||||
var track = new TrackVirtual(10000);
|
||||
mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
clockRate = track.Rate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -86,7 +126,7 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
=> input.OrderBy(h => h.BaseObject.StartTime);
|
||||
|
||||
/// <summary>
|
||||
/// Creates all <see cref="Mod"/> combinations which adjust the <see cref="Beatmap"/> difficulty.
|
||||
/// Creates all <see cref="Mod"/> combinations which adjust the <see cref="Beatmaps.Beatmap"/> difficulty.
|
||||
/// </summary>
|
||||
public Mod[] CreateDifficultyAdjustmentModCombinations()
|
||||
{
|
||||
@ -154,14 +194,15 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all <see cref="Mod"/>s which adjust the <see cref="Beatmap"/> difficulty.
|
||||
/// Retrieves all <see cref="Mod"/>s which adjust the <see cref="Beatmaps.Beatmap"/> difficulty.
|
||||
/// </summary>
|
||||
protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty<Mod>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates <see cref="DifficultyAttributes"/> to describe beatmap's calculated difficulty.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty was calculated.</param>
|
||||
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty was calculated.
|
||||
/// This may differ from <see cref="Beatmap"/> in the case of timed calculation.</param>
|
||||
/// <param name="mods">The <see cref="Mod"/>s that difficulty was calculated with.</param>
|
||||
/// <param name="skills">The skills which processed the beatmap.</param>
|
||||
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
|
||||
@ -178,10 +219,51 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
/// <summary>
|
||||
/// Creates the <see cref="Skill"/>s to calculate the difficulty of an <see cref="IBeatmap"/>.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty will be calculated.</param>
|
||||
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty will be calculated.
|
||||
/// This may differ from <see cref="Beatmap"/> in the case of timed calculation.</param>
|
||||
/// <param name="mods">Mods to calculate difficulty with.</param>
|
||||
/// <param name="clockRate">Clockrate to calculate difficulty with.</param>
|
||||
/// <returns>The <see cref="Skill"/>s.</returns>
|
||||
protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate);
|
||||
|
||||
/// <summary>
|
||||
/// Used to calculate timed difficulty attributes, where only a subset of hitobjects should be visible at any point in time.
|
||||
/// </summary>
|
||||
private class ProgressiveCalculationBeatmap : IBeatmap
|
||||
{
|
||||
private readonly IBeatmap baseBeatmap;
|
||||
|
||||
public ProgressiveCalculationBeatmap(IBeatmap baseBeatmap)
|
||||
{
|
||||
this.baseBeatmap = baseBeatmap;
|
||||
}
|
||||
|
||||
public readonly List<HitObject> HitObjects = new List<HitObject>();
|
||||
|
||||
IReadOnlyList<HitObject> IBeatmap.HitObjects => HitObjects;
|
||||
|
||||
#region Delegated IBeatmap implementation
|
||||
|
||||
public BeatmapInfo BeatmapInfo
|
||||
{
|
||||
get => baseBeatmap.BeatmapInfo;
|
||||
set => baseBeatmap.BeatmapInfo = value;
|
||||
}
|
||||
|
||||
public ControlPointInfo ControlPointInfo
|
||||
{
|
||||
get => baseBeatmap.ControlPointInfo;
|
||||
set => baseBeatmap.ControlPointInfo = value;
|
||||
}
|
||||
|
||||
public BeatmapMetadata Metadata => baseBeatmap.Metadata;
|
||||
public List<BreakPeriod> Breaks => baseBeatmap.Breaks;
|
||||
public double TotalBreakTime => baseBeatmap.TotalBreakTime;
|
||||
public IEnumerable<BeatmapStatistic> GetStatistics() => baseBeatmap.GetStatistics();
|
||||
public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength();
|
||||
public IBeatmap Clone() => new ProgressiveCalculationBeatmap(baseBeatmap.Clone());
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
25
osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs
Normal file
25
osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
|
||||
namespace osu.Game.Rulesets.Difficulty
|
||||
{
|
||||
/// <summary>
|
||||
/// Wraps a <see cref="DifficultyAttributes"/> object and adds a time value for which the attribute is valid.
|
||||
/// Output by <see cref="DifficultyCalculator.CalculateTimed"/>.
|
||||
/// </summary>
|
||||
public class TimedDifficultyAttributes : IComparable<TimedDifficultyAttributes>
|
||||
{
|
||||
public readonly double Time;
|
||||
public readonly DifficultyAttributes Attributes;
|
||||
|
||||
public TimedDifficultyAttributes(double time, DifficultyAttributes attributes)
|
||||
{
|
||||
Time = time;
|
||||
Attributes = attributes;
|
||||
}
|
||||
|
||||
public int CompareTo(TimedDifficultyAttributes other) => Time.CompareTo(other.Time);
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets
|
||||
/// </summary>
|
||||
string InstantiationInfo { get; }
|
||||
|
||||
public Ruleset? CreateInstance()
|
||||
Ruleset? CreateInstance()
|
||||
{
|
||||
var type = Type.GetType(InstantiationInfo);
|
||||
|
||||
|
@ -18,6 +18,11 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public event Action<JudgementResult> NewJudgement;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a judgement is reverted, usually due to rewinding gameplay.
|
||||
/// </summary>
|
||||
public event Action<JudgementResult> JudgementReverted;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of hits that can be judged.
|
||||
/// </summary>
|
||||
@ -71,6 +76,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
JudgedHits--;
|
||||
|
||||
RevertResultInternal(result);
|
||||
|
||||
JudgementReverted?.Invoke(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -66,7 +66,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
||||
|
||||
req.Failure += exception =>
|
||||
{
|
||||
onError?.Invoke(req.Result?.Error ?? exception.Message);
|
||||
onError?.Invoke(req.Response?.Error ?? exception.Message);
|
||||
};
|
||||
|
||||
api.Queue(req);
|
||||
|
@ -1,12 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@ -30,7 +32,12 @@ namespace osu.Game.Screens.Play
|
||||
/// <summary>
|
||||
/// The mods applied to the gameplay.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Mod> Mods;
|
||||
public readonly IReadOnlyList<Mod> Mods;
|
||||
|
||||
/// <summary>
|
||||
/// The gameplay score.
|
||||
/// </summary>
|
||||
public readonly Score Score;
|
||||
|
||||
/// <summary>
|
||||
/// A bindable tracking the last judgement result applied to any hit object.
|
||||
@ -39,11 +46,12 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
|
||||
|
||||
public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList<Mod> mods)
|
||||
public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList<Mod>? mods = null, Score? score = null)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
Ruleset = ruleset;
|
||||
Mods = mods;
|
||||
Score = score ?? new Score();
|
||||
Mods = mods ?? ArraySegment<Mod>.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
219
osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs
Normal file
219
osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs
Normal file
@ -0,0 +1,219 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public class PerformancePointsCounter : RollingCounter<int>, ISkinnableDrawable
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
protected override bool IsRollingProportional => true;
|
||||
|
||||
protected override double RollingDuration => 1000;
|
||||
|
||||
private const float alpha_when_invalid = 0.3f;
|
||||
|
||||
[CanBeNull]
|
||||
[Resolved(CanBeNull = true)]
|
||||
private ScoreProcessor scoreProcessor { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private GameplayState gameplayState { get; set; }
|
||||
|
||||
[CanBeNull]
|
||||
private List<TimedDifficultyAttributes> timedAttributes;
|
||||
|
||||
private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource();
|
||||
|
||||
private JudgementResult lastJudgement;
|
||||
|
||||
public PerformancePointsCounter()
|
||||
{
|
||||
Current.Value = DisplayedCount = 0;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache)
|
||||
{
|
||||
Colour = colours.BlueLighter;
|
||||
|
||||
if (gameplayState != null)
|
||||
{
|
||||
var gameplayWorkingBeatmap = new GameplayWorkingBeatmap(gameplayState.Beatmap);
|
||||
difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, gameplayState.Mods.ToArray(), loadCancellationSource.Token)
|
||||
.ContinueWith(r => Schedule(() =>
|
||||
{
|
||||
timedAttributes = r.Result;
|
||||
IsValid = true;
|
||||
if (lastJudgement != null)
|
||||
onJudgementChanged(lastJudgement);
|
||||
}), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (scoreProcessor != null)
|
||||
{
|
||||
scoreProcessor.NewJudgement += onJudgementChanged;
|
||||
scoreProcessor.JudgementReverted += onJudgementChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private bool isValid;
|
||||
|
||||
protected bool IsValid
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value == isValid)
|
||||
return;
|
||||
|
||||
isValid = value;
|
||||
DrawableCount.FadeTo(isValid ? 1 : alpha_when_invalid, 1000, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
private void onJudgementChanged(JudgementResult judgement)
|
||||
{
|
||||
lastJudgement = judgement;
|
||||
|
||||
var attrib = getAttributeAtTime(judgement);
|
||||
|
||||
if (gameplayState == null || attrib == null)
|
||||
{
|
||||
IsValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(attrib, gameplayState.Score.ScoreInfo);
|
||||
|
||||
Current.Value = (int)Math.Round(calculator?.Calculate() ?? 0, MidpointRounding.AwayFromZero);
|
||||
IsValid = true;
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
private DifficultyAttributes getAttributeAtTime(JudgementResult judgement)
|
||||
{
|
||||
if (timedAttributes == null || timedAttributes.Count == 0)
|
||||
return null;
|
||||
|
||||
int attribIndex = timedAttributes.BinarySearch(new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null));
|
||||
if (attribIndex < 0)
|
||||
attribIndex = ~attribIndex - 1;
|
||||
|
||||
return timedAttributes[Math.Clamp(attribIndex, 0, timedAttributes.Count - 1)].Attributes;
|
||||
}
|
||||
|
||||
protected override LocalisableString FormatCount(int count) => count.ToString(@"D");
|
||||
|
||||
protected override IHasText CreateText() => new TextComponent
|
||||
{
|
||||
Alpha = alpha_when_invalid
|
||||
};
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (scoreProcessor != null)
|
||||
scoreProcessor.NewJudgement -= onJudgementChanged;
|
||||
|
||||
loadCancellationSource?.Cancel();
|
||||
}
|
||||
|
||||
private class TextComponent : CompositeDrawable, IHasText
|
||||
{
|
||||
public LocalisableString Text
|
||||
{
|
||||
get => text.Text;
|
||||
set => text.Text = value;
|
||||
}
|
||||
|
||||
private readonly OsuSpriteText text;
|
||||
|
||||
public TextComponent()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(2),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Font = OsuFont.Numeric.With(size: 16)
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Text = @"pp",
|
||||
Font = OsuFont.Numeric.With(size: 8),
|
||||
Padding = new MarginPadding { Bottom = 1.5f }, // align baseline better
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This class shouldn't exist, but requires breaking changes to allow DifficultyCalculator to receive an IBeatmap.
|
||||
private class GameplayWorkingBeatmap : WorkingBeatmap
|
||||
{
|
||||
private readonly IBeatmap gameplayBeatmap;
|
||||
|
||||
public GameplayWorkingBeatmap(IBeatmap gameplayBeatmap)
|
||||
: base(gameplayBeatmap.BeatmapInfo, null)
|
||||
{
|
||||
this.gameplayBeatmap = gameplayBeatmap;
|
||||
}
|
||||
|
||||
public override IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods = null, TimeSpan? timeout = null)
|
||||
=> gameplayBeatmap;
|
||||
|
||||
protected override IBeatmap GetBeatmap() => gameplayBeatmap;
|
||||
|
||||
protected override Texture GetBackground() => throw new NotImplementedException();
|
||||
|
||||
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
|
||||
|
||||
protected internal override ISkin GetSkin() => throw new NotImplementedException();
|
||||
|
||||
public override Stream GetStream(string storagePath) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
@ -161,13 +161,6 @@ namespace osu.Game.Screens.Play
|
||||
if (!LoadedBeatmapSuccessfully)
|
||||
return;
|
||||
|
||||
Score = CreateScore();
|
||||
|
||||
// ensure the score is in a consistent state with the current player.
|
||||
Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo;
|
||||
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
|
||||
Score.ScoreInfo.Mods = Mods.Value.ToArray();
|
||||
|
||||
PrepareReplay();
|
||||
|
||||
ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(Score.ScoreInfo);
|
||||
@ -225,7 +218,14 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime);
|
||||
|
||||
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, Mods.Value));
|
||||
Score = CreateScore(playableBeatmap);
|
||||
|
||||
// ensure the score is in a consistent state with the current player.
|
||||
Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo;
|
||||
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
|
||||
Score.ScoreInfo.Mods = Mods.Value.ToArray();
|
||||
|
||||
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, Mods.Value, Score));
|
||||
|
||||
AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer));
|
||||
|
||||
@ -988,8 +988,9 @@ namespace osu.Game.Screens.Play
|
||||
/// <summary>
|
||||
/// Creates the player's <see cref="Scoring.Score"/>.
|
||||
/// </summary>
|
||||
/// <param name="beatmap"></param>
|
||||
/// <returns>The <see cref="Scoring.Score"/>.</returns>
|
||||
protected virtual Score CreateScore() => new Score
|
||||
protected virtual Score CreateScore(IBeatmap beatmap) => new Score
|
||||
{
|
||||
ScoreInfo = new ScoreInfo { User = api.LocalUser.Value },
|
||||
};
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play
|
||||
DrawableRuleset?.SetReplayScore(Score);
|
||||
}
|
||||
|
||||
protected override Score CreateScore() => createScore(GameplayState.Beatmap, Mods.Value);
|
||||
protected override Score CreateScore(IBeatmap beatmap) => createScore(beatmap, Mods.Value);
|
||||
|
||||
// Don't re-import replay scores as they're already present in the database.
|
||||
protected override Task ImportScore(Score score) => Task.CompletedTask;
|
||||
|
@ -4,6 +4,7 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Spectator;
|
||||
@ -79,7 +80,7 @@ namespace osu.Game.Screens.Play
|
||||
NonFrameStableSeek(score.Replay.Frames[0].Time);
|
||||
}
|
||||
|
||||
protected override Score CreateScore() => score;
|
||||
protected override Score CreateScore(IBeatmap beatmap) => score;
|
||||
|
||||
protected override ResultsScreen CreateResults(ScoreInfo score)
|
||||
=> new SpectatorResultsScreen(score);
|
||||
|
@ -40,7 +40,7 @@ namespace osu.Game.Screens.Ranking
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuGame game, ScoreModelDownloader scores)
|
||||
private void load(OsuGame game, ScoreManager scores)
|
||||
{
|
||||
InternalChild = shakeContainer = new ShakeContainer
|
||||
{
|
||||
@ -60,7 +60,7 @@ namespace osu.Game.Screens.Ranking
|
||||
break;
|
||||
|
||||
case DownloadState.NotDownloaded:
|
||||
scores.Download(Model.Value);
|
||||
scores.Download(Model.Value, false);
|
||||
break;
|
||||
|
||||
case DownloadState.Importing:
|
||||
|
@ -68,6 +68,7 @@ namespace osu.Game.Skinning
|
||||
var score = container.OfType<DefaultScoreCounter>().FirstOrDefault();
|
||||
var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault();
|
||||
var combo = container.OfType<DefaultComboCounter>().FirstOrDefault();
|
||||
var ppCounter = container.OfType<PerformancePointsCounter>().FirstOrDefault();
|
||||
|
||||
if (score != null)
|
||||
{
|
||||
@ -81,6 +82,13 @@ namespace osu.Game.Skinning
|
||||
|
||||
score.Position = new Vector2(0, vertical_offset);
|
||||
|
||||
if (ppCounter != null)
|
||||
{
|
||||
ppCounter.Y = score.Position.Y + ppCounter.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).Y - 4;
|
||||
ppCounter.Origin = Anchor.TopCentre;
|
||||
ppCounter.Anchor = Anchor.TopCentre;
|
||||
}
|
||||
|
||||
if (accuracy != null)
|
||||
{
|
||||
accuracy.Position = new Vector2(-accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 - horizontal_padding, vertical_offset + 5);
|
||||
@ -123,6 +131,7 @@ namespace osu.Game.Skinning
|
||||
new SongProgress(),
|
||||
new BarHitErrorMeter(),
|
||||
new BarHitErrorMeter(),
|
||||
new PerformancePointsCounter()
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens;
|
||||
@ -32,6 +33,9 @@ namespace osu.Game.Tests.Visual
|
||||
content = new Container { RelativeSizeAxes = Axes.Both },
|
||||
DialogOverlay = new DialogOverlay()
|
||||
});
|
||||
|
||||
Stack.ScreenPushed += (lastScreen, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}");
|
||||
Stack.ScreenExited += (lastScreen, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed ← {newScreen}");
|
||||
}
|
||||
|
||||
protected void LoadScreen(OsuScreen screen) => Stack.Push(screen);
|
||||
|
@ -36,7 +36,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="10.6.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2021.929.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2021.1004.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
|
||||
<PackageReference Include="Sentry" Version="3.9.4" />
|
||||
<PackageReference Include="SharpCompress" Version="0.29.0" />
|
||||
|
@ -70,7 +70,7 @@
|
||||
<Reference Include="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.929.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.1004.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
|
||||
</ItemGroup>
|
||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
||||
@ -93,7 +93,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2021.929.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2021.1004.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.28.3" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||
|
Loading…
Reference in New Issue
Block a user