2019-01-24 16:43:03 +08:00
// 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.
2018-04-13 17:19:50 +08:00
2022-06-17 15:37:17 +08:00
#nullable disable
2020-06-18 12:20:16 +08:00
using System ;
using System.Collections.Generic ;
2019-11-04 08:52:26 +08:00
using System.Linq ;
2020-06-19 14:35:39 +08:00
using System.Threading.Tasks ;
2018-04-13 17:19:50 +08:00
using NUnit.Framework ;
using osu.Framework.Allocation ;
2019-11-04 08:52:26 +08:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
using osu.Framework.Screens ;
2020-06-19 14:35:39 +08:00
using osu.Framework.Testing ;
2020-06-19 17:02:57 +08:00
using osu.Framework.Utils ;
2018-04-13 17:19:50 +08:00
using osu.Game.Beatmaps ;
2022-01-07 22:55:53 +08:00
using osu.Game.Database ;
2020-08-30 00:30:56 +08:00
using osu.Game.Graphics.UserInterface ;
2020-06-18 12:20:16 +08:00
using osu.Game.Online.API ;
2022-01-07 22:55:53 +08:00
using osu.Game.Rulesets ;
2022-03-02 01:21:32 +08:00
using osu.Game.Rulesets.Difficulty ;
using osu.Game.Rulesets.Osu ;
2018-11-28 15:12:57 +08:00
using osu.Game.Scoring ;
2019-11-04 08:52:26 +08:00
using osu.Game.Screens ;
2018-12-21 15:28:33 +08:00
using osu.Game.Screens.Play ;
2018-04-13 17:19:50 +08:00
using osu.Game.Screens.Ranking ;
2022-03-02 01:21:32 +08:00
using osu.Game.Screens.Ranking.Expanded.Statistics ;
2020-06-19 17:02:57 +08:00
using osu.Game.Screens.Ranking.Statistics ;
2021-12-13 15:34:48 +08:00
using osu.Game.Tests.Resources ;
2020-06-23 14:21:23 +08:00
using osuTK ;
2020-06-19 14:35:39 +08:00
using osuTK.Input ;
2022-01-07 22:55:53 +08:00
using Realms ;
2018-04-13 17:19:50 +08:00
2020-03-17 16:43:16 +08:00
namespace osu.Game.Tests.Visual.Ranking
2018-04-13 17:19:50 +08:00
{
[TestFixture]
2022-11-24 13:32:20 +08:00
public partial class TestSceneResultsScreen : OsuManualInputManagerTestScene
2018-04-13 17:19:50 +08:00
{
2021-05-21 16:28:25 +08:00
[Resolved]
private BeatmapManager beatmaps { get ; set ; }
2018-04-13 17:19:50 +08:00
2022-01-07 22:55:53 +08:00
[Resolved]
2022-01-24 18:59:58 +08:00
private RealmAccess realm { get ; set ; }
2022-01-07 22:55:53 +08:00
2018-04-13 17:19:50 +08:00
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2022-01-25 12:04:05 +08:00
realm . Run ( r = >
2022-01-07 22:55:53 +08:00
{
2022-01-25 12:04:05 +08:00
var beatmapInfo = r . All < BeatmapInfo > ( )
. Filter ( $"{nameof(BeatmapInfo.Ruleset)}.{nameof(RulesetInfo.OnlineID)} = $0" , 0 )
. FirstOrDefault ( ) ;
2022-01-07 22:55:53 +08:00
if ( beatmapInfo ! = null )
Beatmap . Value = beatmaps . GetWorkingBeatmap ( beatmapInfo ) ;
2022-01-21 16:08:20 +08:00
} ) ;
2019-11-04 08:52:26 +08:00
}
2018-04-13 17:19:50 +08:00
2022-05-08 21:00:07 +08:00
[Test]
public void TestScaling ( )
{
// scheduling is needed as scaling the content immediately causes the entire scene to shake badly, for some odd reason.
AddSliderStep ( "scale" , 0.5f , 1.6f , 1f , v = > Schedule ( ( ) = >
{
Content . Scale = new Vector2 ( v ) ;
Content . Size = new Vector2 ( 1f / v ) ;
} ) ) ;
}
2023-07-14 00:53:32 +08:00
private int onlineScoreID = 1 ;
2023-07-13 00:24:09 +08:00
[TestCase(1, ScoreRank.X)]
[TestCase(0.9999, ScoreRank.S)]
[TestCase(0.975, ScoreRank.S)]
[TestCase(0.925, ScoreRank.A)]
[TestCase(0.85, ScoreRank.B)]
[TestCase(0.75, ScoreRank.C)]
[TestCase(0.5, ScoreRank.D)]
[TestCase(0.2, ScoreRank.D)]
public void TestResultsWithPlayer ( double accuracy , ScoreRank rank )
{
TestResultsScreen screen = null ;
2023-07-14 00:53:32 +08:00
loadResultsScreen ( ( ) = >
{
var score = TestResources . CreateTestScoreInfo ( ) ;
2023-07-13 00:24:09 +08:00
2023-07-14 00:53:32 +08:00
score . OnlineID = onlineScoreID + + ;
score . HitEvents = TestSceneStatisticsPanel . CreatePositionDistributedHitEvents ( ) ;
score . Accuracy = accuracy ;
score . Rank = rank ;
2023-07-13 00:24:09 +08:00
2023-07-14 00:53:32 +08:00
return screen = createResultsScreen ( score ) ;
} ) ;
2023-07-13 00:24:09 +08:00
AddUntilStep ( "wait for loaded" , ( ) = > screen . IsLoaded ) ;
AddAssert ( "retry overlay present" , ( ) = > screen . RetryOverlay ! = null ) ;
}
2019-11-04 08:52:26 +08:00
[Test]
2020-06-19 14:35:39 +08:00
public void TestResultsWithoutPlayer ( )
2019-11-04 08:52:26 +08:00
{
2020-06-03 10:06:59 +08:00
TestResultsScreen screen = null ;
2020-01-31 18:10:44 +08:00
OsuScreenStack stack ;
2019-11-04 08:52:26 +08:00
2020-01-31 18:10:44 +08:00
AddStep ( "load results" , ( ) = >
2019-11-04 08:52:26 +08:00
{
2020-01-31 18:10:44 +08:00
Child = stack = new OsuScreenStack
{
RelativeSizeAxes = Axes . Both
} ;
2023-07-13 00:06:26 +08:00
var score = TestResources . CreateTestScoreInfo ( ) ;
stack . Push ( screen = createResultsScreen ( score ) ) ;
2019-11-04 08:52:26 +08:00
} ) ;
AddUntilStep ( "wait for loaded" , ( ) = > screen . IsLoaded ) ;
AddAssert ( "retry overlay not present" , ( ) = > screen . RetryOverlay = = null ) ;
}
2020-06-03 10:06:59 +08:00
[Test]
2020-06-19 14:35:39 +08:00
public void TestResultsForUnranked ( )
2020-06-03 10:06:59 +08:00
{
UnrankedSoloResultsScreen screen = null ;
2022-05-11 23:26:04 +08:00
loadResultsScreen ( ( ) = > screen = createUnrankedSoloResultsScreen ( ) ) ;
2020-06-03 10:06:59 +08:00
AddUntilStep ( "wait for loaded" , ( ) = > screen . IsLoaded ) ;
AddAssert ( "retry overlay present" , ( ) = > screen . RetryOverlay ! = null ) ;
}
2020-06-23 14:21:23 +08:00
[Test]
public void TestShowHideStatisticsViaOutsideClick ( )
{
TestResultsScreen screen = null ;
2022-05-11 23:26:04 +08:00
loadResultsScreen ( ( ) = > screen = createResultsScreen ( ) ) ;
2021-09-06 19:20:52 +08:00
AddUntilStep ( "wait for load" , ( ) = > this . ChildrenOfType < ScorePanelList > ( ) . Single ( ) . AllPanelsVisible ) ;
2020-06-23 14:21:23 +08:00
AddStep ( "click expanded panel" , ( ) = >
{
var expandedPanel = this . ChildrenOfType < ScorePanel > ( ) . Single ( p = > p . State = = PanelState . Expanded ) ;
InputManager . MoveMouseTo ( expandedPanel ) ;
InputManager . Click ( MouseButton . Left ) ;
} ) ;
AddAssert ( "statistics shown" , ( ) = > this . ChildrenOfType < StatisticsPanel > ( ) . Single ( ) . State . Value = = Visibility . Visible ) ;
AddUntilStep ( "expanded panel at the left of the screen" , ( ) = >
{
var expandedPanel = this . ChildrenOfType < ScorePanel > ( ) . Single ( p = > p . State = = PanelState . Expanded ) ;
return expandedPanel . ScreenSpaceDrawQuad . TopLeft . X - screen . ScreenSpaceDrawQuad . TopLeft . X < 150 ;
} ) ;
AddStep ( "click to right of panel" , ( ) = >
{
var expandedPanel = this . ChildrenOfType < ScorePanel > ( ) . Single ( p = > p . State = = PanelState . Expanded ) ;
2022-02-05 22:41:04 +08:00
InputManager . MoveMouseTo ( expandedPanel . ScreenSpaceDrawQuad . TopRight + new Vector2 ( 50 , 0 ) ) ;
2020-06-23 14:21:23 +08:00
InputManager . Click ( MouseButton . Left ) ;
} ) ;
AddAssert ( "statistics hidden" , ( ) = > this . ChildrenOfType < StatisticsPanel > ( ) . Single ( ) . State . Value = = Visibility . Hidden ) ;
AddUntilStep ( "expanded panel in centre of screen" , ( ) = >
{
var expandedPanel = this . ChildrenOfType < ScorePanel > ( ) . Single ( p = > p . State = = PanelState . Expanded ) ;
return Precision . AlmostEquals ( expandedPanel . ScreenSpaceDrawQuad . Centre . X , screen . ScreenSpaceDrawQuad . Centre . X , 1 ) ;
} ) ;
}
2020-06-19 17:02:57 +08:00
[Test]
public void TestShowHideStatistics ( )
{
TestResultsScreen screen = null ;
2022-05-11 23:26:04 +08:00
loadResultsScreen ( ( ) = > screen = createResultsScreen ( ) ) ;
2021-09-06 19:20:52 +08:00
AddUntilStep ( "wait for load" , ( ) = > this . ChildrenOfType < ScorePanelList > ( ) . Single ( ) . AllPanelsVisible ) ;
2020-06-19 17:02:57 +08:00
AddStep ( "click expanded panel" , ( ) = >
{
var expandedPanel = this . ChildrenOfType < ScorePanel > ( ) . Single ( p = > p . State = = PanelState . Expanded ) ;
InputManager . MoveMouseTo ( expandedPanel ) ;
InputManager . Click ( MouseButton . Left ) ;
} ) ;
AddAssert ( "statistics shown" , ( ) = > this . ChildrenOfType < StatisticsPanel > ( ) . Single ( ) . State . Value = = Visibility . Visible ) ;
AddUntilStep ( "expanded panel at the left of the screen" , ( ) = >
{
var expandedPanel = this . ChildrenOfType < ScorePanel > ( ) . Single ( p = > p . State = = PanelState . Expanded ) ;
return expandedPanel . ScreenSpaceDrawQuad . TopLeft . X - screen . ScreenSpaceDrawQuad . TopLeft . X < 150 ;
} ) ;
AddStep ( "click expanded panel" , ( ) = >
{
var expandedPanel = this . ChildrenOfType < ScorePanel > ( ) . Single ( p = > p . State = = PanelState . Expanded ) ;
InputManager . MoveMouseTo ( expandedPanel ) ;
InputManager . Click ( MouseButton . Left ) ;
} ) ;
AddAssert ( "statistics hidden" , ( ) = > this . ChildrenOfType < StatisticsPanel > ( ) . Single ( ) . State . Value = = Visibility . Hidden ) ;
AddUntilStep ( "expanded panel in centre of screen" , ( ) = >
{
var expandedPanel = this . ChildrenOfType < ScorePanel > ( ) . Single ( p = > p . State = = PanelState . Expanded ) ;
return Precision . AlmostEquals ( expandedPanel . ScreenSpaceDrawQuad . Centre . X , screen . ScreenSpaceDrawQuad . Centre . X , 1 ) ;
} ) ;
}
[Test]
public void TestShowStatisticsAndClickOtherPanel ( )
{
TestResultsScreen screen = null ;
2022-05-11 23:26:04 +08:00
loadResultsScreen ( ( ) = > screen = createResultsScreen ( ) ) ;
2021-09-06 19:20:52 +08:00
AddUntilStep ( "wait for load" , ( ) = > this . ChildrenOfType < ScorePanelList > ( ) . Single ( ) . AllPanelsVisible ) ;
2020-06-19 17:02:57 +08:00
ScorePanel expandedPanel = null ;
ScorePanel contractedPanel = null ;
AddStep ( "click expanded panel then contracted panel" , ( ) = >
{
expandedPanel = this . ChildrenOfType < ScorePanel > ( ) . Single ( p = > p . State = = PanelState . Expanded ) ;
InputManager . MoveMouseTo ( expandedPanel ) ;
InputManager . Click ( MouseButton . Left ) ;
contractedPanel = this . ChildrenOfType < ScorePanel > ( ) . First ( p = > p . State = = PanelState . Contracted & & p . ScreenSpaceDrawQuad . TopLeft . X > screen . ScreenSpaceDrawQuad . TopLeft . X ) ;
InputManager . MoveMouseTo ( contractedPanel ) ;
InputManager . Click ( MouseButton . Left ) ;
} ) ;
AddAssert ( "statistics shown" , ( ) = > this . ChildrenOfType < StatisticsPanel > ( ) . Single ( ) . State . Value = = Visibility . Visible ) ;
AddAssert ( "contracted panel still contracted" , ( ) = > contractedPanel . State = = PanelState . Contracted ) ;
AddAssert ( "expanded panel still expanded" , ( ) = > expandedPanel . State = = PanelState . Expanded ) ;
}
2020-06-19 14:35:39 +08:00
[Test]
public void TestFetchScoresAfterShowingStatistics ( )
{
DelayedFetchResultsScreen screen = null ;
2022-01-15 00:37:58 +08:00
var tcs = new TaskCompletionSource < bool > ( ) ;
2021-12-21 14:14:49 +08:00
2022-05-11 23:26:04 +08:00
loadResultsScreen ( ( ) = > screen = new DelayedFetchResultsScreen ( TestResources . CreateTestScoreInfo ( ) , tcs . Task ) ) ;
2021-12-21 14:14:49 +08:00
2020-06-19 14:35:39 +08:00
AddUntilStep ( "wait for loaded" , ( ) = > screen . IsLoaded ) ;
2021-12-21 14:14:49 +08:00
2020-06-19 14:35:39 +08:00
AddStep ( "click expanded panel" , ( ) = >
{
var expandedPanel = this . ChildrenOfType < ScorePanel > ( ) . Single ( p = > p . State = = PanelState . Expanded ) ;
InputManager . MoveMouseTo ( expandedPanel ) ;
InputManager . Click ( MouseButton . Left ) ;
} ) ;
2021-12-21 14:14:49 +08:00
AddAssert ( "no fetch yet" , ( ) = > ! screen . FetchCompleted ) ;
2022-01-15 00:37:58 +08:00
AddStep ( "allow fetch" , ( ) = > tcs . SetResult ( true ) ) ;
2021-12-21 14:14:49 +08:00
2020-06-19 14:35:39 +08:00
AddUntilStep ( "wait for fetch" , ( ) = > screen . FetchCompleted ) ;
AddAssert ( "expanded panel still on screen" , ( ) = > this . ChildrenOfType < ScorePanel > ( ) . Single ( p = > p . State = = PanelState . Expanded ) . ScreenSpaceDrawQuad . TopLeft . X > 0 ) ;
}
2020-08-30 00:30:56 +08:00
[Test]
2020-08-30 02:13:03 +08:00
public void TestDownloadButtonInitiallyDisabled ( )
2020-08-30 00:30:56 +08:00
{
TestResultsScreen screen = null ;
2022-05-11 23:26:04 +08:00
loadResultsScreen ( ( ) = > screen = createResultsScreen ( ) ) ;
2021-09-06 19:20:52 +08:00
AddUntilStep ( "wait for load" , ( ) = > this . ChildrenOfType < ScorePanelList > ( ) . Single ( ) . AllPanelsVisible ) ;
2020-08-30 00:30:56 +08:00
2020-09-24 12:17:03 +08:00
AddAssert ( "download button is disabled" , ( ) = > ! screen . ChildrenOfType < DownloadButton > ( ) . Last ( ) . Enabled . Value ) ;
2020-08-30 00:30:56 +08:00
AddStep ( "click contracted panel" , ( ) = >
{
var contractedPanel = this . ChildrenOfType < ScorePanel > ( ) . First ( p = > p . State = = PanelState . Contracted & & p . ScreenSpaceDrawQuad . TopLeft . X > screen . ScreenSpaceDrawQuad . TopLeft . X ) ;
InputManager . MoveMouseTo ( contractedPanel ) ;
InputManager . Click ( MouseButton . Left ) ;
} ) ;
2020-09-24 12:17:03 +08:00
AddAssert ( "download button is enabled" , ( ) = > screen . ChildrenOfType < DownloadButton > ( ) . Last ( ) . Enabled . Value ) ;
2020-08-30 00:30:56 +08:00
}
2022-03-02 01:21:32 +08:00
[Test]
public void TestRulesetWithNoPerformanceCalculator ( )
{
var ruleset = new RulesetWithNoPerformanceCalculator ( ) ;
var score = TestResources . CreateTestScoreInfo ( ruleset . RulesetInfo ) ;
2022-05-11 23:26:04 +08:00
loadResultsScreen ( ( ) = > createResultsScreen ( score ) ) ;
2022-03-02 01:21:32 +08:00
AddUntilStep ( "wait for load" , ( ) = > this . ChildrenOfType < ScorePanelList > ( ) . Single ( ) . AllPanelsVisible ) ;
AddAssert ( "PP displayed as 0" , ( ) = >
{
var performance = this . ChildrenOfType < PerformanceStatistic > ( ) . Single ( ) ;
var counter = performance . ChildrenOfType < StatisticCounter > ( ) . Single ( ) ;
return counter . Current . Value = = 0 ;
} ) ;
}
2022-05-11 23:26:04 +08:00
private void loadResultsScreen ( Func < ResultsScreen > createResults )
{
ResultsScreen results = null ;
AddStep ( "load results" , ( ) = > Child = new TestResultsContainer ( results = createResults ( ) ) ) ;
// expanded panel should be centered the moment results screen is loaded
// but can potentially be scrolled away on certain specific load scenarios.
// see: https://github.com/ppy/osu/issues/18226
AddUntilStep ( "expanded panel in centre of screen" , ( ) = >
{
var expandedPanel = this . ChildrenOfType < ScorePanel > ( ) . Single ( p = > p . State = = PanelState . Expanded ) ;
return Precision . AlmostEquals ( expandedPanel . ScreenSpaceDrawQuad . Centre . X , results . ScreenSpaceDrawQuad . Centre . X , 1 ) ;
} ) ;
}
2021-12-13 15:37:20 +08:00
private TestResultsScreen createResultsScreen ( ScoreInfo score = null ) = > new TestResultsScreen ( score ? ? TestResources . CreateTestScoreInfo ( ) ) ;
2021-05-21 16:28:25 +08:00
2021-12-13 15:37:20 +08:00
private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen ( ) = > new UnrankedSoloResultsScreen ( TestResources . CreateTestScoreInfo ( ) ) ;
2021-05-21 16:28:25 +08:00
2022-11-24 13:32:20 +08:00
private partial class TestResultsContainer : Container
2019-11-04 08:52:26 +08:00
{
[Cached(typeof(Player))]
private readonly Player player = new TestPlayer ( ) ;
public TestResultsContainer ( IScreen screen )
{
RelativeSizeAxes = Axes . Both ;
2020-01-31 18:10:44 +08:00
OsuScreenStack stack ;
2019-11-04 08:52:26 +08:00
2020-01-31 18:10:44 +08:00
InternalChild = stack = new OsuScreenStack
2018-04-13 17:19:50 +08:00
{
2019-11-04 08:52:26 +08:00
RelativeSizeAxes = Axes . Both ,
} ;
2020-01-31 18:10:44 +08:00
stack . Push ( screen ) ;
2019-11-04 08:52:26 +08:00
}
}
2023-07-13 00:24:09 +08:00
private partial class TestResultsScreen : SoloResultsScreen
2020-06-03 10:06:59 +08:00
{
public HotkeyRetryOverlay RetryOverlay ;
public TestResultsScreen ( ScoreInfo score )
2020-11-21 21:38:38 +08:00
: base ( score , true )
2020-06-03 10:06:59 +08:00
{
2023-07-13 00:24:09 +08:00
ShowUserStatistics = true ;
2020-06-03 10:06:59 +08:00
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
RetryOverlay = InternalChildren . OfType < HotkeyRetryOverlay > ( ) . SingleOrDefault ( ) ;
}
2020-06-18 12:20:16 +08:00
protected override APIRequest FetchScores ( Action < IEnumerable < ScoreInfo > > scoresCallback )
{
var scores = new List < ScoreInfo > ( ) ;
for ( int i = 0 ; i < 20 ; i + + )
{
2021-12-13 15:37:20 +08:00
var score = TestResources . CreateTestScoreInfo ( ) ;
2020-06-18 12:20:16 +08:00
score . TotalScore + = 10 - i ;
2023-10-27 19:22:17 +08:00
score . HasOnlineReplay = true ;
2020-06-18 12:20:16 +08:00
scores . Add ( score ) ;
}
scoresCallback ? . Invoke ( scores ) ;
return null ;
}
2020-06-03 10:06:59 +08:00
}
2022-11-24 13:32:20 +08:00
private partial class DelayedFetchResultsScreen : TestResultsScreen
2020-06-19 14:35:39 +08:00
{
2021-12-21 14:14:49 +08:00
private readonly Task fetchWaitTask ;
2020-06-19 14:35:39 +08:00
2021-12-21 14:14:49 +08:00
public bool FetchCompleted { get ; private set ; }
2020-06-19 14:35:39 +08:00
2021-12-21 14:14:49 +08:00
public DelayedFetchResultsScreen ( ScoreInfo score , Task fetchWaitTask = null )
2020-06-19 14:35:39 +08:00
: base ( score )
{
2021-12-21 14:14:49 +08:00
this . fetchWaitTask = fetchWaitTask ? ? Task . CompletedTask ;
2020-06-19 14:35:39 +08:00
}
protected override APIRequest FetchScores ( Action < IEnumerable < ScoreInfo > > scoresCallback )
{
Task . Run ( async ( ) = >
{
2021-12-21 14:14:49 +08:00
await fetchWaitTask ;
2020-06-19 14:35:39 +08:00
var scores = new List < ScoreInfo > ( ) ;
for ( int i = 0 ; i < 20 ; i + + )
{
2021-12-13 15:34:48 +08:00
var score = TestResources . CreateTestScoreInfo ( ) ;
2020-06-19 14:35:39 +08:00
score . TotalScore + = 10 - i ;
scores . Add ( score ) ;
}
scoresCallback ? . Invoke ( scores ) ;
Schedule ( ( ) = > FetchCompleted = true ) ;
} ) ;
return null ;
}
}
2022-11-24 13:32:20 +08:00
private partial class UnrankedSoloResultsScreen : SoloResultsScreen
2019-11-04 08:52:26 +08:00
{
public HotkeyRetryOverlay RetryOverlay ;
2020-06-03 10:06:59 +08:00
public UnrankedSoloResultsScreen ( ScoreInfo score )
2020-11-21 21:36:59 +08:00
: base ( score , true )
2019-11-04 08:52:26 +08:00
{
2023-07-05 04:39:26 +08:00
Score . BeatmapInfo ! . OnlineID = 0 ;
2021-11-24 17:42:47 +08:00
Score . BeatmapInfo . Status = BeatmapOnlineStatus . Pending ;
2019-11-04 08:52:26 +08:00
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
RetryOverlay = InternalChildren . OfType < HotkeyRetryOverlay > ( ) . SingleOrDefault ( ) ;
}
2018-04-13 17:19:50 +08:00
}
2022-03-02 01:21:32 +08:00
private class RulesetWithNoPerformanceCalculator : OsuRuleset
{
2022-03-14 13:25:26 +08:00
public override PerformanceCalculator CreatePerformanceCalculator ( ) = > null ;
2022-03-02 01:21:32 +08:00
}
2018-04-13 17:19:50 +08:00
}
}