Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 147 additions & 45 deletions fluxel.FallbackScoreSubmission/API/ScoresRoute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,84 @@ public async Task Handle(FluxelAPIInteraction interaction)
return;
}

if (payload.Scores.Count == 0)
if (payload.Scores.Count != map.PlayerCount)
{
await interaction.ReplyMessage(HttpStatusCode.BadRequest, "score contains no players");
await interaction.ReplyMessage(HttpStatusCode.BadRequest, "invalid player count");
return;
}

//only handle the first player for now
Score userScore = new Score
Score userScore = createScoreFromPayload(payload, map, rate);

if (!allPlayersValid(userScore))
{
UserID = payload.Scores[0].UserID,
MapHash = payload.MapHash,
MapID = map.ID,
ScrollSpeed = payload.Scores[0].ScrollSpeed,
Mods = string.Join(",", payload.Mods),
await interaction.ReplyMessage(HttpStatusCode.BadRequest, "invalid users in score"); //log more info?
return;
}

//get users old stats
List<UserStats> previousUserStats = new()
{
new()
{
Ovr = userScore.User.OverallRating,
Prt = userScore.User.PotentialRating,
Rank = userScore.User.GetGlobalRank()
}
};

foreach (var extraPlayer in userScore.ExtraPlayers)
{
previousUserStats.Add(new()
{
Ovr = extraPlayer.User.OverallRating,
Prt = extraPlayer.User.PotentialRating,
Rank = extraPlayer.User.GetGlobalRank(),
});
}

//check judgement counts
if (userScore.JudgementCount != userScore.Map.MaxComboForPlayer(0))
{
await interaction.ReplyMessage(HttpStatusCode.BadRequest, "player 0 judgement count doesn't match the map's hit object count");
return;
}

int playerIndex = 1;

foreach (var extraPlayer in userScore.ExtraPlayers)
{
if (extraPlayer.JudgementCount != userScore.Map.MaxComboForPlayer(playerIndex))
{
//Console.WriteLine("player " + playerIndex + " judgement count doesn't match the map's hit object count");
//Console.WriteLine("expected " + userScore.Map.MaxComboForPlayer(playerIndex) + ", got " + extraPlayer.JudgementCount);
await interaction.ReplyMessage(HttpStatusCode.BadRequest, "player " + playerIndex + " judgement count doesn't match the map's hit object count");
return;
}

playerIndex++;
}

//submit score
ScoreHelper.Add(userScore);

//save replay
string replayJson = JsonSerializer.Serialize(payload.Replay);
var replayBytes = Encoding.UTF8.GetBytes(replayJson);
Assets.WriteAsset(AssetType.Replay, $"{userScore.ID}", replayBytes, "", "frp");

//recalculate ptr/ovr/rank
try
{
UserHelper.UpdateLocked(userScore.UserID, u => u.Recalculate());
foreach (var extraPlayer in userScore.ExtraPlayers) UserHelper.UpdateLocked(extraPlayer.UserID, u => u.Recalculate());
}
catch (Exception e)
{
await interaction.ReplyMessage(HttpStatusCode.InternalServerError, "failed to recalculate user stats");
return;
}

//get new stats TODO: adpapt this for multiple players?
User? user = UserHelper.Get(userScore.UserID);

if (user == null)
Expand All @@ -77,26 +139,36 @@ public async Task Handle(FluxelAPIInteraction interaction)
return;
}

//get user old stats
double prevOvr = user.OverallRating;
double prevPrt = user.PotentialRating;
int prevRank = user.GetGlobalRank();
ScoreSubmissionStats response = new ScoreSubmissionStats(userScore.ToAPI(), previousUserStats[0].Ovr, previousUserStats[0].Prt, previousUserStats[0].Rank, user.OverallRating, user.PotentialRating, user.GetGlobalRank());

await interaction.Reply(HttpStatusCode.OK, response);
}

private Score createScoreFromPayload(ScoreSubmissionPayload payload, Map map, float rate)
{
Score userScore = new Score
{
UserID = payload.Scores[0].UserID,
MapHash = payload.MapHash,
MapID = map.ID,
ScrollSpeed = payload.Scores[0].ScrollSpeed,
Mods = string.Join(",", payload.Mods),
};
for (int i = 1; i < payload.Scores.Count; i++) userScore.ExtraPlayers.Add(new() { UserID = payload.Scores[i].UserID, Score = userScore });

//handle results
HitWindows hitWindows = new HitWindows(map.AccuracyDifficulty, rate);
ReleaseWindows releaseWindows = new ReleaseWindows(map.AccuracyDifficulty, rate);
int combo = 0;
int maxCombo = 0;
int judgementCount = 0;

//first user
foreach (var result in payload.Scores[0].Results)
{
Judgement judgement = result.HoldEnd
? releaseWindows.JudgementFor(result.Difference)
: hitWindows.JudgementFor(result.Difference);

combo++;
judgementCount++;

switch (judgement)
{
Expand All @@ -119,45 +191,75 @@ public async Task Handle(FluxelAPIInteraction interaction)
if (combo > maxCombo) maxCombo = combo;
}

//make sure the judgement count is correct
if (judgementCount != userScore.Map.MaxCombo)
{
await interaction.ReplyMessage(HttpStatusCode.BadRequest, "judgement count doesn't match the map's hit object count");
return;
}

//submit score
userScore.MaxCombo = maxCombo;
userScore.Recalculate();
ScoreHelper.Add(userScore);

//save replay
string replayJson = JsonSerializer.Serialize(payload.Replay);
var replayBytes = Encoding.UTF8.GetBytes(replayJson);
Assets.WriteAsset(AssetType.Replay, $"{userScore.ID}", replayBytes, "", "frp");

//recalculate ptr/ovr/rank
try
//all other users
for (int i = 1; i < payload.Scores.Count; i++)
{
UserHelper.UpdateLocked(userScore.UserID, u => u.Recalculate());
}
catch (Exception e)
{
await interaction.ReplyMessage(HttpStatusCode.InternalServerError, "failed to recalculate user stats");
return;
ScoreExtraPlayer scoreExtraPlayer = userScore.ExtraPlayers[i - 1];

combo = 0;
maxCombo = 0;

foreach (var result in payload.Scores[i].Results)
{
Judgement judgement = result.HoldEnd
? releaseWindows.JudgementFor(result.Difference)
: hitWindows.JudgementFor(result.Difference);

combo++;

switch (judgement)
{
case Judgement.Flawless: scoreExtraPlayer.FlawlessCount++; break;

case Judgement.Perfect: scoreExtraPlayer.PerfectCount++; break;

case Judgement.Alright: scoreExtraPlayer.AlrightCount++; break;

case Judgement.Great: scoreExtraPlayer.GreatCount++; break;

case Judgement.Okay: scoreExtraPlayer.OkayCount++; break;

case Judgement.Miss:
combo = 0;
scoreExtraPlayer.MissCount++;
break;
}

if (combo > maxCombo) maxCombo = combo;
}

scoreExtraPlayer.MaxCombo = maxCombo;
scoreExtraPlayer.Recalculate(i);
}

//get new stats (might not be needed if the previous one somehow gets updated?)
user = UserHelper.Get(userScore.UserID);
return userScore;
}

if (user == null)
private bool allPlayersValid(Score score)
{
if (score.User == null) return false;

foreach (var extraPlayer in score.ExtraPlayers)
{
await interaction.ReplyMessage(HttpStatusCode.BadRequest, "failed to get user");
return;
if (extraPlayer.User == null) return false;

//if (score.UserID == extraPlayer.UserID) return false; //uncomment this to prevent local scores from being submitted (we might want to do also do that client side)
}

ScoreSubmissionStats response = new ScoreSubmissionStats(userScore.ToAPI(), prevOvr, prevPrt, prevRank, user.OverallRating, user.PotentialRating, user.GetGlobalRank());
return true;
}

await interaction.Reply(HttpStatusCode.OK, response);
public class UserStats
{
public double Ovr { get; set; }
public double Prt { get; set; }
public int Rank { get; set; }

public UserStats()
{
}
}
}
5 changes: 3 additions & 2 deletions fluxel.Multiplayer/MultiplayerSocket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,11 @@ public async Task FinishPlay(ScoreInfo score)
if (player == null)
return;

score.PlayerID = UserID;
//TODO: adapt for dual
score.Players[0].PlayerID = UserID;
setPlayerStatus(player.ID, MultiplayerUserState.Finished);

score.HitResults = new List<HitResult>();
score.Players[0].HitResults = new List<HitResult>();
player.Score = score;

await endIfAllFinished();
Expand Down
66 changes: 53 additions & 13 deletions fluxel/API/Routes/Maps/MapLeaderboardRoute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,22 @@ public async Task Handle(FluxelAPIInteraction interaction)
var type = interaction.GetStringQuery("type") ?? "global";
var version = interaction.GetStringQuery("version") ?? map.SHA256Hash;

//assume index 0 by default to not break previous versions of the game
long playerIndex = 0;
interaction.TryGetLongQuery("playerIndex", out playerIndex);
if (playerIndex >= map.PlayerCount) playerIndex = 0;

switch (type)
{
case "global":
{
var all = ScoreHelper.FromMap(map, version).ToList();
all.ForEach(s => s.Cache = interaction.Cache);

reply(interaction, set, map, filterList(all.OrderByDescending(s => s.PerformanceRating).ToList()));
reply(interaction, set, map,
playerIndex == 0
? filterList(all.OrderByDescending(s => s.PerformanceRating).ToList())
: filterList(all.OrderByDescending(s => s.ExtraPlayers[(int)playerIndex - 1].PerformanceRating).ToList()));
break;
}

Expand All @@ -64,7 +72,7 @@ public async Task Handle(FluxelAPIInteraction interaction)
return;
}

reply(interaction, set, map, getCountry(map, version, interaction.User.CountryCode));
reply(interaction, set, map, getCountry(map, version, interaction.User.CountryCode, (int)playerIndex));
break;

case "club":
Expand All @@ -74,20 +82,25 @@ public async Task Handle(FluxelAPIInteraction interaction)
return;
}

reply(interaction, set, map, getClub(map, version, interaction.User.Club.ID));
reply(interaction, set, map, getClub(map, version, interaction.User.Club.ID, (int)playerIndex));
break;

case "friends":
{
var following = RelationHelper.GetFollowing(interaction.User.ID);
following.Add(interaction.UserID);

var all = ScoreHelper.FromMap(map, version).Where(s => following.Contains(s.UserID)).ToList();
var all =
playerIndex == 0
? ScoreHelper.FromMap(map, version).Where(s => following.Contains(s.UserID)).ToList()
: ScoreHelper.FromMap(map, version).Where(s => following.Contains(s.ExtraPlayers[(int)playerIndex - 1].UserID)).ToList();

reply(interaction, set, map, filterList(all.OrderByDescending(s => s.PerformanceRating).ToList()));
break;
}

case "clubs":
//TODO: consider dual maps?
await interaction.Reply(HttpStatusCode.OK, new MapLeaderboardClubs(map.ToAPI(), ClubHelper.GetScoresOnMap(map.ID).OrderByDescending(s => s.PerformanceRating).Select(s => s.ToAPI())));
break;

Expand All @@ -107,6 +120,12 @@ public async Task Handle(FluxelAPIInteraction interaction)
if (interaction.UserID != -1)
api.User.Following = RelationHelper.IsFollowing(interaction.UserID, api.User.ID);

foreach (var apiScoreExtraPlayer in api.ExtraPlayers)
{
if (interaction.UserID != -1)
apiScoreExtraPlayer.User.Following = RelationHelper.IsFollowing(interaction.UserID, apiScoreExtraPlayer.User.ID);
}

return api;
})));

Expand All @@ -128,16 +147,37 @@ private static List<Score> filterList(List<Score> all)
return scores;
}

private List<Score> getCountry(Map map, string? version, string code)
=> filterList(ScoreHelper.FromMap(map, version)
.Where(s => s.User?.CountryCode == code)
.OrderByDescending(s => s.PerformanceRating).ToList());
private List<Score> getCountry(Map map, string? version, string code, int playerIndex)
{
if (playerIndex == 0)
{
return filterList(ScoreHelper.FromMap(map, version)
.Where(s => s.User?.CountryCode == code)
.OrderByDescending(s => s.PerformanceRating).ToList());
}
else
{
return filterList(ScoreHelper.FromMap(map, version)
.Where(s => s.ExtraPlayers[playerIndex - 1].User?.CountryCode == code)
.OrderByDescending(s => s.ExtraPlayers[playerIndex - 1].PerformanceRating).ToList());
}
}

private List<Score> getClub(Map map, string? version, long id)
private List<Score> getClub(Map map, string? version, long id, int playerIndex)
{
return filterList(ScoreHelper.FromMap(map, version)
.Where(s => s.User?.Club?.ID == id)
.OrderByDescending(s => s.PerformanceRating)
.ToList());
if (playerIndex == 0)
{
return filterList(ScoreHelper.FromMap(map, version)
.Where(s => s.User?.Club?.ID == id)
.OrderByDescending(s => s.PerformanceRating)
.ToList());
}
else
{
return filterList(ScoreHelper.FromMap(map, version)
.Where(s => s.ExtraPlayers[playerIndex - 1].User?.Club?.ID == id)
.OrderByDescending(s => s.ExtraPlayers[playerIndex - 1].PerformanceRating)
.ToList());
}
}
}
Loading