From a653c039cf2332e98f43d662e937ecbce2614ac5 Mon Sep 17 00:00:00 2001 From: orwenn22 <46846090+orwenn22@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:47:43 -0500 Subject: [PATCH 1/2] adapt to new ScoreInfo structure --- fluxel.Multiplayer/MultiplayerSocket.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fluxel.Multiplayer/MultiplayerSocket.cs b/fluxel.Multiplayer/MultiplayerSocket.cs index 08efc7ba..c0b52465 100644 --- a/fluxel.Multiplayer/MultiplayerSocket.cs +++ b/fluxel.Multiplayer/MultiplayerSocket.cs @@ -171,10 +171,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(); + score.Players[0].HitResults = new List(); player.Score = score; await endIfAllFinished(); From cad03903e8d03e67db859da203cbecde0565b029 Mon Sep 17 00:00:00 2001 From: orwenn22 <46846090+orwenn22@users.noreply.github.com> Date: Sat, 29 Nov 2025 11:06:31 -0500 Subject: [PATCH 2/2] update models to support dual maps/scores + update MapLeaderboardRoute to filter by playerindex + score submission (kinda) --- .../API/ScoresRoute.cs | 192 ++++++++++++++---- fluxel/API/Routes/Maps/MapLeaderboardRoute.cs | 66 ++++-- fluxel/Database/Extensions/ScoreExtensions.cs | 23 +++ fluxel/Database/Helpers/ScoreHelper.cs | 2 + fluxel/Models/Maps/Map.cs | 31 +++ fluxel/Models/Scores/Score.cs | 6 + fluxel/Models/Scores/ScoreExtraPlayer.cs | 93 +++++++++ fluxel/Tasks/Maps/RecalculateMapTask.cs | 21 +- fluxel/Utils/ScoreUtils.cs | 40 +++- fluxel/Utils/ServerMapUtils.cs | 59 ++++-- 10 files changed, 441 insertions(+), 92 deletions(-) create mode 100644 fluxel/Models/Scores/ScoreExtraPlayer.cs diff --git a/fluxel.FallbackScoreSubmission/API/ScoresRoute.cs b/fluxel.FallbackScoreSubmission/API/ScoresRoute.cs index 14f3e58e..8dd39f06 100644 --- a/fluxel.FallbackScoreSubmission/API/ScoresRoute.cs +++ b/fluxel.FallbackScoreSubmission/API/ScoresRoute.cs @@ -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 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) @@ -77,18 +139,29 @@ 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 @@ -96,7 +169,6 @@ public async Task Handle(FluxelAPIInteraction interaction) : hitWindows.JudgementFor(result.Difference); combo++; - judgementCount++; switch (judgement) { @@ -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() + { + } } } diff --git a/fluxel/API/Routes/Maps/MapLeaderboardRoute.cs b/fluxel/API/Routes/Maps/MapLeaderboardRoute.cs index 2be62a1f..d7b32c31 100644 --- a/fluxel/API/Routes/Maps/MapLeaderboardRoute.cs +++ b/fluxel/API/Routes/Maps/MapLeaderboardRoute.cs @@ -46,6 +46,11 @@ 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": @@ -53,7 +58,10 @@ public async Task Handle(FluxelAPIInteraction interaction) 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; } @@ -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": @@ -74,7 +82,7 @@ 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": @@ -82,12 +90,17 @@ public async Task Handle(FluxelAPIInteraction interaction) 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; @@ -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; }))); @@ -128,16 +147,37 @@ private static List filterList(List all) return scores; } - private List 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 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 getClub(Map map, string? version, long id) + private List 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()); + } } } diff --git a/fluxel/Database/Extensions/ScoreExtensions.cs b/fluxel/Database/Extensions/ScoreExtensions.cs index c9b49003..d9887e2d 100644 --- a/fluxel/Database/Extensions/ScoreExtensions.cs +++ b/fluxel/Database/Extensions/ScoreExtensions.cs @@ -30,6 +30,29 @@ public static APIScore ToAPI(this Score score, List? include = de ScrollSpeed = score.ScrollSpeed }; + foreach (var scoreExtraPlayer in score.ExtraPlayers) + { + var apiScoreExtraPlayer = new APIScoreExtraPlayer + { + User = scoreExtraPlayer.APIUser, + PerformanceRating = scoreExtraPlayer.PerformanceRating, + TotalScore = scoreExtraPlayer.TotalScore, + Accuracy = scoreExtraPlayer.Accuracy, + Rank = scoreExtraPlayer.Grade, + MaxCombo = scoreExtraPlayer.MaxCombo, + FlawlessCount = scoreExtraPlayer.FlawlessCount, + PerfectCount = scoreExtraPlayer.PerfectCount, + GreatCount = scoreExtraPlayer.GreatCount, + AlrightCount = scoreExtraPlayer.AlrightCount, + OkayCount = scoreExtraPlayer.OkayCount, + MissCount = scoreExtraPlayer.MissCount, + ScrollSpeed = scoreExtraPlayer.ScrollSpeed, + Score = apiScore + }; + + apiScore.ExtraPlayers.Add(apiScoreExtraPlayer); + } + if (include == null || include.Count == 0) return apiScore; diff --git a/fluxel/Database/Helpers/ScoreHelper.cs b/fluxel/Database/Helpers/ScoreHelper.cs index f8f13601..0994f524 100644 --- a/fluxel/Database/Helpers/ScoreHelper.cs +++ b/fluxel/Database/Helpers/ScoreHelper.cs @@ -8,6 +8,8 @@ namespace fluxel.Database.Helpers; +//TODO: right now when querying anything from here the Score field of the ScoreExtraPlayers won't be filled +//(but maybe that's not a big deal, since currently it is only useful when processing the score?) public static class ScoreHelper { private static IMongoCollection scores => MongoDatabase.GetCollection("scores"); diff --git a/fluxel/Models/Maps/Map.cs b/fluxel/Models/Maps/Map.cs index bb7c2699..0936f138 100644 --- a/fluxel/Models/Maps/Map.cs +++ b/fluxel/Models/Maps/Map.cs @@ -75,12 +75,17 @@ public class Map : IHasCache [BsonElement("rating")] public double Rating { get; set; } + //these are the hits only for the main player. For dual maps, see "DualSides" [BsonElement("hits")] public int Hits { get; set; } + //these are the lns only for the main player. For dual maps, see "DualSides" [BsonElement("lns")] public int LongNotes { get; set; } + [BsonElement("dual-sides")] + public List DualSides { get; set; } = new(); + [BsonElement("effects")] public MapEffectType Effects { get; set; } @@ -90,6 +95,9 @@ public class Map : IHasCache [BsonElement("votes")] public Dictionary? Votes { get; set; } = new(); + [BsonElement("player-count")] + public int PlayerCount { get; set; } = 1; + [BsonIgnore] public int MaxCombo => Hits + LongNotes * 2; @@ -146,6 +154,29 @@ public double RecalculateRating() var effectRate = (readTotal + trackTotal + perceptTotal) / 3 / count; return Rating = baseRate + effectRate * 2; } + + public int MaxComboForPlayer(int playerIndex) + { + if (playerIndex < 0 || playerIndex >= PlayerCount) return 0; + if (playerIndex == 0) return MaxCombo; + + int dualIndex = playerIndex - 1; + if (dualIndex >= DualSides.Count) return 0; + + return DualSides[dualIndex].MaxCombo; + } + + public class MapDualSide + { + [BsonElement("hits")] + public int Hits { get; set; } + + [BsonElement("lns")] + public int LongNotes { get; set; } + + [BsonIgnore] + public int MaxCombo => Hits + LongNotes * 2; + } } [Flags] diff --git a/fluxel/Models/Scores/Score.cs b/fluxel/Models/Scores/Score.cs index 5a5e977f..ac327e7d 100644 --- a/fluxel/Models/Scores/Score.cs +++ b/fluxel/Models/Scores/Score.cs @@ -95,12 +95,18 @@ public class Score [BsonElement("miss")] public int MissCount { get; set; } + [BsonIgnore] + public int JudgementCount => FlawlessCount + PerfectCount + GreatCount + AlrightCount + OkayCount + MissCount; + [BsonElement("scrollspeed")] public float ScrollSpeed { get; set; } [BsonElement("replay")] public bool HasReplay { get; set; } + [BsonElement("extra-players")] + public List ExtraPlayers { get; set; } = new(); + [BsonIgnore] public RequestCache Cache { get; set; } = new(); diff --git a/fluxel/Models/Scores/ScoreExtraPlayer.cs b/fluxel/Models/Scores/ScoreExtraPlayer.cs new file mode 100644 index 00000000..c572dc32 --- /dev/null +++ b/fluxel/Models/Scores/ScoreExtraPlayer.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq; +using fluxel.API.Components; +using fluxel.Database.Extensions; +using fluxel.Database.Helpers; +using fluxel.Models.Users; +using fluxel.Utils; +using fluXis.Online.API.Models.Users; +using fluXis.Scoring.Processing; +using fluXis.Utils; +using MongoDB.Bson.Serialization.Attributes; + +namespace fluxel.Models.Scores; + +public class ScoreExtraPlayer +{ + [BsonIgnore] + public Score? Score { get; set; } + + [BsonElement("user")] + public long UserID { get; init; } + + [BsonIgnore] + public User? User => Cache.Users.Get(UserID) ?? UserHelper.Get(UserID); + + [BsonIgnore] + public APIUser APIUser => User?.ToAPI() ?? APIUser.CreateUnknown(UserID); + + [BsonElement("pr")] + public double PerformanceRating { get; set; } + + [BsonElement("score")] + public int TotalScore { get; set; } + + [BsonElement("accuracy")] + public float Accuracy { get; set; } + + [BsonElement("grade")] + public string Grade { get; set; } = null!; + + [BsonElement("combo")] + public int MaxCombo { get; set; } + + [BsonElement("flawless")] + public int FlawlessCount { get; set; } + + [BsonElement("perfect")] + public int PerfectCount { get; set; } + + [BsonElement("great")] + public int GreatCount { get; set; } + + [BsonElement("alright")] + public int AlrightCount { get; set; } + + [BsonElement("okay")] + public int OkayCount { get; set; } + + [BsonElement("miss")] + public int MissCount { get; set; } + + [BsonIgnore] + public int JudgementCount => FlawlessCount + PerfectCount + GreatCount + AlrightCount + OkayCount + MissCount; + + [BsonElement("scrollspeed")] + public float ScrollSpeed { get; set; } + + [BsonIgnore] + public RequestCache Cache { get; set; } = new(); + + public void Recalculate(int playerIndex) + { + if (Score is null) + { + throw new Exception("Score is null"); + } + + Accuracy = this.CalculateAccuracy(); + TotalScore = this.CalculateScore(playerIndex); + PerformanceRating = ScoreProcessor.CalculatePerformance( + (float)Score.Map.Rating, + Accuracy, + FlawlessCount, + PerfectCount, + GreatCount, + AlrightCount, + OkayCount, + MissCount, + Score.ModList.Select(ModUtils.GetFromAcronym).ToList() + ); + Grade = this.GetGrade(); + } +} diff --git a/fluxel/Tasks/Maps/RecalculateMapTask.cs b/fluxel/Tasks/Maps/RecalculateMapTask.cs index 9e380f48..609a10e2 100644 --- a/fluxel/Tasks/Maps/RecalculateMapTask.cs +++ b/fluxel/Tasks/Maps/RecalculateMapTask.cs @@ -1,6 +1,6 @@ using System; -using System.Linq; using fluxel.Database.Helpers; +using fluxel.Models.Maps; using fluxel.Utils; using fluXis.Online.API.Models.Maps; using fluXis.Utils; @@ -55,13 +55,20 @@ public void Run() dbMap.SHA256Hash = MapUtils.GetHash(map.RawContent); dbMap.NotesPerSecond = MapUtils.GetNps(map.HitObjects); - dbMap.Hits = map.HitObjects.Count(x => x.Type switch + dbMap.Hits = map.HitsForPlayer(0); + dbMap.LongNotes = map.LongNotesForPlayer(0); + + dbMap.PlayerCount = map.PlayerCount; + dbMap.DualSides.Clear(); + + for (int i = 1; i < map.PlayerCount; i++) { - 0 => x.HoldTime <= 0, - 1 => true, - _ => false - }); - dbMap.LongNotes = map.HitObjects.Count(x => x.Type == 0 && x.HoldTime > 0); + dbMap.DualSides.Add(new Map.MapDualSide + { + Hits = map.HitsForPlayer(i), + LongNotes = map.LongNotesForPlayer(i) + }); + } dbMap.AccuracyDifficulty = map.AccuracyDifficulty; dbMap.HealthDifficulty = map.HealthDifficulty; diff --git a/fluxel/Utils/ScoreUtils.cs b/fluxel/Utils/ScoreUtils.cs index 7460f6bd..aaf6f6c6 100644 --- a/fluxel/Utils/ScoreUtils.cs +++ b/fluxel/Utils/ScoreUtils.cs @@ -7,9 +7,17 @@ public static class ScoreUtils { public static int CalculateScore(this Score score) { - var maxScore = (int)(1000000 * getMulitpliers(score)); + var maxScore = (int)(1000000 * getMultipliers(score)); var accBased = (int)(score.Accuracy / 100f * (maxScore * .9f)); - var comboBased = (int)(score.MaxCombo / (float)score.Map.MaxCombo * (maxScore * .1f)); + var comboBased = (int)(score.MaxCombo / (float)score.Map.MaxComboForPlayer(0) * (maxScore * .1f)); + return accBased + comboBased; + } + + public static int CalculateScore(this ScoreExtraPlayer score, int playerIndex) + { + var maxScore = (int)(1000000 * getMultipliers(score.Score)); + var accBased = (int)(score.Accuracy / 100f * (maxScore * .9f)); + var comboBased = (int)(score.MaxCombo / (float)score.Score.Map.MaxComboForPlayer(playerIndex) * (maxScore * .1f)); return accBased + comboBased; } @@ -27,6 +35,20 @@ public static float CalculateAccuracy(this Score score) return rated / total * 100f; } + public static float CalculateAccuracy(this ScoreExtraPlayer score) + { + var rated = 0f; + var total = score.FlawlessCount + score.PerfectCount + score.GreatCount + score.AlrightCount + score.OkayCount + score.MissCount; + + rated += score.FlawlessCount; + rated += score.PerfectCount * .98f; + rated += score.GreatCount * .65f; + rated += score.AlrightCount * .25f; + rated += score.OkayCount * .1f; + + return rated / total * 100f; + } + public static float CalculatePerformanceRating(this Score score) { var totalScore = score.TotalScore; @@ -39,10 +61,14 @@ public static float CalculatePerformanceRating(this Score score) }; } - private static float getMulitpliers(this Score score) + private static float getMultipliers(this Score score) { var mods = score.Mods.Split(","); + return getMultipliers(mods); + } + private static float getMultipliers(string[] mods) + { var multiplier = 1f; foreach (var mod in mods) @@ -77,9 +103,13 @@ private static float getMulitpliers(this Score score) return multiplier; } - public static string GetGrade(this Score score) + public static string GetGrade(this Score score) => getGrade(score.Accuracy); + + public static string GetGrade(this ScoreExtraPlayer score) => getGrade(score.Accuracy); + + private static string getGrade(float accuracy) { - return score.Accuracy switch + return accuracy switch { 100 => "X", >= 99 => "SS", diff --git a/fluxel/Utils/ServerMapUtils.cs b/fluxel/Utils/ServerMapUtils.cs index 9c35355c..2fb8a9c1 100644 --- a/fluxel/Utils/ServerMapUtils.cs +++ b/fluxel/Utils/ServerMapUtils.cs @@ -16,29 +16,44 @@ namespace fluxel.Utils; public static class ServerMapUtils { - public static Map CreateFromJson(MapInfo json, long id, long set, string entry, string hash, long mapper, string effects, string storyboard) => new() + public static Map CreateFromJson(MapInfo json, long id, long set, string entry, string hash, long mapper, string effects, string storyboard) { - ID = id, - SetID = set, - FileName = entry, - SHA256Hash = hash, - EffectSHA256Hash = string.IsNullOrEmpty(effects) ? "" : MapUtils.GetHash(effects), - StoryboardSHA256Hash = string.IsNullOrEmpty(storyboard) ? "" : MapUtils.GetHash(storyboard), - MapperID = mapper, - Title = json.Metadata.Title, - TitleRomanized = json.Metadata.TitleRomanized, - Artist = json.Metadata.Artist, - ArtistRomanized = json.Metadata.ArtistRomanized, - Source = json.Metadata.AudioSource, - Tags = json.Metadata.Tags, - BPM = json.TimingPoints.First().BPM, - DifficultyName = json.Metadata.Difficulty, - Mode = json.KeyCount, - Length = (int)json.HitObjects.Max(h => h.Time), - Hits = json.HitObjects.Count(h => h.HoldTime == 0), - LongNotes = json.HitObjects.Count(h => h.HoldTime > 0) * 2, - NotesPerSecond = MapUtils.GetNps(json.HitObjects) - }; + var map = new Map + { + ID = id, + SetID = set, + FileName = entry, + SHA256Hash = hash, + EffectSHA256Hash = string.IsNullOrEmpty(effects) ? "" : MapUtils.GetHash(effects), + StoryboardSHA256Hash = string.IsNullOrEmpty(storyboard) ? "" : MapUtils.GetHash(storyboard), + MapperID = mapper, + Title = json.Metadata.Title, + TitleRomanized = json.Metadata.TitleRomanized, + Artist = json.Metadata.Artist, + ArtistRomanized = json.Metadata.ArtistRomanized, + Source = json.Metadata.AudioSource, + Tags = json.Metadata.Tags, + BPM = json.TimingPoints.First().BPM, + DifficultyName = json.Metadata.Difficulty, + Mode = json.SinglePlayerKeyCount, + Length = (int)json.HitObjects.Max(h => h.Time), + Hits = json.HitsForPlayer(0), + LongNotes = json.LongNotesForPlayer(0), + NotesPerSecond = MapUtils.GetNps(json.HitObjects), //TODO: distinct nps for each dual sides? + PlayerCount = json.PlayerCount + }; + + for (int i = 1; i < json.PlayerCount; ++i) + { + map.DualSides.Add(new() + { + Hits = json.HitsForPlayer(i), + LongNotes = json.LongNotesForPlayer(i) + }); + } + + return map; + } public static bool ReadFile(this ZipArchive archive, string? name, [NotNullWhen(true)] out string? content) {