1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-28 08:43:20 +08:00

Rankings tables implementation (#6293)

Rankings tables implementation

Co-authored-by: Dean Herbert <pe@ppy.sh>
This commit is contained in:
Dean Herbert 2019-11-29 16:12:24 +09:00 committed by GitHub
commit 4dc8e0ae20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 651 additions and 2 deletions

View File

@ -0,0 +1,129 @@
// 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.Graphics.Containers;
using osu.Game.Overlays.Rankings.Tables;
using osu.Framework.Graphics;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osu.Game.Graphics.UserInterface;
using System.Threading;
using osu.Game.Online.API;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.Catch;
using osu.Framework.Allocation;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneRankingsTables : OsuTestScene
{
protected override bool UseOnlineAPI => true;
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(PerformanceTable),
typeof(ScoresTable),
typeof(CountriesTable),
typeof(TableRowBackground),
typeof(UserBasedTable),
typeof(RankingsTable<>)
};
[Resolved]
private IAPIProvider api { get; set; }
private readonly BasicScrollContainer scrollFlow;
private readonly DimmedLoadingLayer loading;
private CancellationTokenSource cancellationToken;
private APIRequest request;
public TestSceneRankingsTables()
{
Children = new Drawable[]
{
scrollFlow = new BasicScrollContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Width = 0.8f,
},
loading = new DimmedLoadingLayer(),
};
}
protected override void LoadComplete()
{
base.LoadComplete();
AddStep("Osu performance", () => createPerformanceTable(new OsuRuleset().RulesetInfo, null));
AddStep("Mania scores", () => createScoreTable(new ManiaRuleset().RulesetInfo));
AddStep("Taiko country scores", () => createCountryTable(new TaikoRuleset().RulesetInfo));
AddStep("Catch US performance page 10", () => createPerformanceTable(new CatchRuleset().RulesetInfo, "US", 10));
}
private void createCountryTable(RulesetInfo ruleset, int page = 1)
{
onLoadStarted();
request = new GetCountryRankingsRequest(ruleset, page);
((GetCountryRankingsRequest)request).Success += rankings => Schedule(() =>
{
var table = new CountriesTable(page, rankings.Countries);
loadTable(table);
});
api.Queue(request);
}
private void createPerformanceTable(RulesetInfo ruleset, string country, int page = 1)
{
onLoadStarted();
request = new GetUserRankingsRequest(ruleset, country: country, page: page);
((GetUserRankingsRequest)request).Success += rankings => Schedule(() =>
{
var table = new PerformanceTable(page, rankings.Users);
loadTable(table);
});
api.Queue(request);
}
private void createScoreTable(RulesetInfo ruleset, int page = 1)
{
onLoadStarted();
request = new GetUserRankingsRequest(ruleset, UserRankingsType.Score, page);
((GetUserRankingsRequest)request).Success += rankings => Schedule(() =>
{
var table = new ScoresTable(page, rankings.Users);
loadTable(table);
});
api.Queue(request);
}
private void onLoadStarted()
{
loading.Show();
request?.Cancel();
cancellationToken?.Cancel();
cancellationToken = new CancellationTokenSource();
}
private void loadTable(Drawable table)
{
LoadComponentAsync(table, t =>
{
scrollFlow.Clear();
scrollFlow.Add(t);
loading.Hide();
}, cancellationToken.Token);
}
}
}

View File

@ -0,0 +1,15 @@
// 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 Newtonsoft.Json;
using osu.Game.Users;
namespace osu.Game.Online.API.Requests
{
public class GetCountriesResponse : ResponseWithCursor
{
[JsonProperty("ranking")]
public List<CountryStatistics> Countries;
}
}

View File

@ -0,0 +1,17 @@
// 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.Game.Rulesets;
namespace osu.Game.Online.API.Requests
{
public class GetCountryRankingsRequest : GetRankingsRequest<GetCountriesResponse>
{
public GetCountryRankingsRequest(RulesetInfo ruleset, int page = 1)
: base(ruleset, page)
{
}
protected override string TargetPostfix() => "country";
}
}

View File

@ -0,0 +1,33 @@
// 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.IO.Network;
using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests
{
public abstract class GetRankingsRequest<TModel> : APIRequest<TModel>
{
private readonly RulesetInfo ruleset;
private readonly int page;
protected GetRankingsRequest(RulesetInfo ruleset, int page = 1)
{
this.ruleset = ruleset;
this.page = page;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.AddParameter("page", page.ToString());
return req;
}
protected override string Target => $"rankings/{ruleset.ShortName}/{TargetPostfix()}";
protected abstract string TargetPostfix();
}
}

View File

@ -0,0 +1,39 @@
// 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.IO.Network;
using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests
{
public class GetUserRankingsRequest : GetRankingsRequest<GetUsersResponse>
{
private readonly string country;
private readonly UserRankingsType type;
public GetUserRankingsRequest(RulesetInfo ruleset, UserRankingsType type = UserRankingsType.Performance, int page = 1, string country = null)
: base(ruleset, page)
{
this.type = type;
this.country = country;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
if (country != null)
req.AddParameter("country", country);
return req;
}
protected override string TargetPostfix() => type.ToString().ToLowerInvariant();
}
public enum UserRankingsType
{
Performance,
Score
}
}

View File

@ -3,13 +3,13 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Users;
namespace osu.Game.Online.API.Requests
{
public class GetUsersResponse : ResponseWithCursor
{
[JsonProperty("ranking")]
public List<APIUser> Users;
public List<UserStatistics> Users;
}
}

View File

@ -0,0 +1,67 @@
// 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;
using osu.Framework.Graphics.Containers;
using System;
using osu.Game.Users;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics;
using System.Collections.Generic;
namespace osu.Game.Overlays.Rankings.Tables
{
public class CountriesTable : RankingsTable<CountryStatistics>
{
public CountriesTable(int page, IReadOnlyList<CountryStatistics> rankings)
: base(page, rankings)
{
}
protected override TableColumn[] CreateAdditionalHeaders() => new[]
{
new TableColumn("Active Users", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
new TableColumn("Play Count", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
new TableColumn("Ranked Score", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
new TableColumn("Avg. Score", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
new TableColumn("Performance", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
new TableColumn("Avg. Perf.", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
};
protected override Country GetCountry(CountryStatistics item) => item.Country;
protected override Drawable CreateFlagContent(CountryStatistics item) => new OsuSpriteText
{
Font = OsuFont.GetFont(size: TEXT_SIZE),
Text = $@"{item.Country.FullName}",
};
protected override Drawable[] CreateAdditionalContent(CountryStatistics item) => new Drawable[]
{
new ColoredRowText
{
Text = $@"{item.ActiveUsers:N0}",
},
new ColoredRowText
{
Text = $@"{item.PlayCount:N0}",
},
new ColoredRowText
{
Text = $@"{item.RankedScore:N0}",
},
new ColoredRowText
{
Text = $@"{item.RankedScore / Math.Max(item.ActiveUsers, 1):N0}",
},
new RowText
{
Text = $@"{item.Performance:N0}",
},
new ColoredRowText
{
Text = $@"{item.Performance / Math.Max(item.ActiveUsers, 1):N0}",
}
};
}
}

View File

@ -0,0 +1,28 @@
// 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 osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Users;
namespace osu.Game.Overlays.Rankings.Tables
{
public class PerformanceTable : UserBasedTable
{
public PerformanceTable(int page, IReadOnlyList<UserStatistics> rankings)
: base(page, rankings)
{
}
protected override TableColumn[] CreateUniqueHeaders() => new[]
{
new TableColumn("Performance", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
};
protected override Drawable[] CreateUniqueContent(UserStatistics item) => new Drawable[]
{
new RowText { Text = $@"{item.PP:N0}", }
};
}
}

View File

@ -0,0 +1,140 @@
// 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;
using osu.Framework.Graphics.Containers;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osuTK;
namespace osu.Game.Overlays.Rankings.Tables
{
public abstract class RankingsTable<TModel> : TableContainer
{
protected const int TEXT_SIZE = 14;
private const float horizontal_inset = 20;
private const float row_height = 25;
private const int items_per_page = 50;
private readonly int page;
private readonly IReadOnlyList<TModel> rankings;
protected RankingsTable(int page, IReadOnlyList<TModel> rankings)
{
this.page = page;
this.rankings = rankings;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding { Horizontal = horizontal_inset };
RowSize = new Dimension(GridSizeMode.Absolute, row_height);
}
[BackgroundDependencyLoader]
private void load()
{
FillFlowContainer backgroundFlow;
AddInternal(backgroundFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Depth = 1f,
Margin = new MarginPadding { Top = row_height }
});
rankings.ForEach(_ => backgroundFlow.Add(new TableRowBackground()));
Columns = mainHeaders.Concat(CreateAdditionalHeaders()).ToArray();
Content = rankings.Select((s, i) => createContent((page - 1) * items_per_page + i, s)).ToArray().ToRectangular();
}
private Drawable[] createContent(int index, TModel item) => new Drawable[] { createIndexDrawable(index), createMainContent(item) }.Concat(CreateAdditionalContent(item)).ToArray();
private static TableColumn[] mainHeaders => new[]
{
new TableColumn(string.Empty, Anchor.Centre, new Dimension(GridSizeMode.Absolute, 50)), // place
new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed)), // flag and username (country name)
};
protected abstract TableColumn[] CreateAdditionalHeaders();
protected abstract Drawable[] CreateAdditionalContent(TModel item);
protected override Drawable CreateHeader(int index, TableColumn column) => new HeaderText(column?.Header ?? string.Empty, HighlightedColumn());
protected abstract Country GetCountry(TModel item);
protected abstract Drawable CreateFlagContent(TModel item);
private OsuSpriteText createIndexDrawable(int index) => new OsuSpriteText
{
Text = $"#{index + 1}",
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold)
};
private FillFlowContainer createMainContent(TModel item) => new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(7, 0),
Children = new[]
{
new UpdateableFlag(GetCountry(item))
{
Size = new Vector2(20, 13),
ShowPlaceholderOnNull = false,
},
CreateFlagContent(item)
}
};
protected virtual string HighlightedColumn() => @"Performance";
private class HeaderText : OsuSpriteText
{
private readonly string highlighted;
public HeaderText(string text, string highlighted)
{
this.highlighted = highlighted;
Text = text;
Font = OsuFont.GetFont(size: 12);
Margin = new MarginPadding { Horizontal = 10 };
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
if (Text != highlighted)
Colour = colours.GreySeafoamLighter;
}
}
protected class RowText : OsuSpriteText
{
public RowText()
{
Font = OsuFont.GetFont(size: TEXT_SIZE);
Margin = new MarginPadding { Horizontal = 10 };
}
}
protected class ColoredRowText : RowText
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.GreySeafoamLighter;
}
}
}
}

View File

@ -0,0 +1,38 @@
// 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 osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Users;
namespace osu.Game.Overlays.Rankings.Tables
{
public class ScoresTable : UserBasedTable
{
public ScoresTable(int page, IReadOnlyList<UserStatistics> rankings)
: base(page, rankings)
{
}
protected override TableColumn[] CreateUniqueHeaders() => new[]
{
new TableColumn("Total Score", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
new TableColumn("Ranked Score", Anchor.Centre, new Dimension(GridSizeMode.AutoSize))
};
protected override Drawable[] CreateUniqueContent(UserStatistics item) => new Drawable[]
{
new ColoredRowText
{
Text = $@"{item.TotalScore:N0}",
},
new RowText
{
Text = $@"{item.RankedScore:N0}",
}
};
protected override string HighlightedColumn() => @"Ranked Score";
}
}

View File

@ -0,0 +1,56 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Overlays.Rankings.Tables
{
public class TableRowBackground : CompositeDrawable
{
private const int fade_duration = 100;
private readonly Box background;
private Color4 idleColour;
private Color4 hoverColour;
public TableRowBackground()
{
RelativeSizeAxes = Axes.X;
Height = 25;
CornerRadius = 3;
Masking = true;
InternalChild = background = new Box
{
RelativeSizeAxes = Axes.Both,
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
background.Colour = idleColour = colours.GreySeafoam;
hoverColour = colours.GreySeafoamLight;
}
protected override bool OnHover(HoverEvent e)
{
background.FadeColour(hoverColour, fade_duration, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
background.FadeColour(idleColour, fade_duration, Easing.OutQuint);
base.OnHoverLost(e);
}
}
}

View File

@ -0,0 +1,56 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Users;
namespace osu.Game.Overlays.Rankings.Tables
{
public abstract class UserBasedTable : RankingsTable<UserStatistics>
{
protected UserBasedTable(int page, IReadOnlyList<UserStatistics> rankings)
: base(page, rankings)
{
}
protected override TableColumn[] CreateAdditionalHeaders() => new[]
{
new TableColumn("Accuracy", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
new TableColumn("Play Count", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
}.Concat(CreateUniqueHeaders()).Concat(new[]
{
new TableColumn("SS", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
new TableColumn("S", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
new TableColumn("A", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
}).ToArray();
protected sealed override Country GetCountry(UserStatistics item) => item.User.Country;
protected sealed override Drawable CreateFlagContent(UserStatistics item)
{
var username = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: TEXT_SIZE)) { AutoSizeAxes = Axes.Both };
username.AddUserLink(item.User);
return username;
}
protected sealed override Drawable[] CreateAdditionalContent(UserStatistics item) => new[]
{
new ColoredRowText { Text = $@"{item.Accuracy:F2}%", },
new ColoredRowText { Text = $@"{item.PlayCount:N0}", },
}.Concat(CreateUniqueContent(item)).Concat(new[]
{
new ColoredRowText { Text = $@"{item.GradesCount.SS + item.GradesCount.SSPlus:N0}", },
new ColoredRowText { Text = $@"{item.GradesCount.S + item.GradesCount.SPlus:N0}", },
new ColoredRowText { Text = $@"{item.GradesCount.A:N0}", }
}).ToArray();
protected abstract TableColumn[] CreateUniqueHeaders();
protected abstract Drawable[] CreateUniqueContent(UserStatistics item);
}
}

View File

@ -0,0 +1,28 @@
// 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 Newtonsoft.Json;
namespace osu.Game.Users
{
public class CountryStatistics
{
[JsonProperty]
public Country Country;
[JsonProperty(@"code")]
public string FlagName;
[JsonProperty(@"active_users")]
public long ActiveUsers;
[JsonProperty(@"play_count")]
public long PlayCount;
[JsonProperty(@"ranked_score")]
public long RankedScore;
[JsonProperty(@"performance")]
public long Performance;
}
}

View File

@ -10,6 +10,9 @@ namespace osu.Game.Users
{
public class UserStatistics
{
[JsonProperty]
public User User;
[JsonProperty(@"level")]
public LevelInfo Level;