/// <summary>
        /// Parses a detailed summary entry into a scoreboard details object.
        /// </summary>
        /// <param name="dataEntries">The data.</param>
        protected virtual void ParseDetailedSummaryEntry(ScoreboardDetails details, string[] dataEntries)
        {
            var summary = new ScoreboardSummaryEntry();

            details.Summary = summary;
            // ID, Division (labeled location, their bug), Location (labeled division, their bug), tier, scored image count, play time, score time, warnings, current score
            summary.TeamId = TeamId.Parse(dataEntries[0]);
            // [not in data, matched from categoryProvider] all service category
            summary.Category = _categoryProvider?.GetCategory(summary.TeamId);
            // tier and division
            if (Utilities.TryParseEnumSpaceless <Division>(dataEntries[2], out Division division))
            {
                summary.Division = division;
            }
            summary.Location = dataEntries[1];
            if (Enum.TryParse <Tier>(dataEntries[3], true, out Tier tier))
            {
                summary.Tier = tier;
            }
            // number of images
            summary.ImageCount = int.Parse(dataEntries[4].Trim());
            // times
            summary.PlayTime  = Utilities.ParseHourMinuteSecondTimespan(dataEntries[5]);
            details.ScoreTime = Utilities.ParseHourMinuteSecondTimespan(dataEntries[6]);
            // warnings and total score
            string warnStr = dataEntries[7];

            summary.Warnings  |= warnStr.Contains("T") ? ScoreWarnings.TimeOver : 0;
            summary.Warnings  |= warnStr.Contains("M") ? ScoreWarnings.MultiImage : 0;
            summary.TotalScore = double.Parse(dataEntries.Last().Trim());
        }
        public async Task GetTeamWithPercentileAsync(double rank, DivisionWithCategory?divAndCat = null, Tier?tier = null)
        {
            using (Context.Channel.EnterTypingState())
            {
                if (rank < 1)
                {
                    throw new ArgumentOutOfRangeException(nameof(rank));
                }

                var teams = await ScoreRetrievalService.GetScoreboardAsync(new ScoreboardFilterInfo(divAndCat?.Division, tier, divAndCat?.Category, null)).ConfigureAwait(false);

                // teams list in descending order
                int expectedIndex           = ((int)Math.Round(((100 - rank) / 100) * teams.TeamList.Count)).Clamp(0, teams.TeamList.Count);
                ScoreboardDetails teamScore = await ScoreRetrievalService.GetDetailsAsync(teams.TeamList[expectedIndex].TeamId).ConfigureAwait(false);

                if (teamScore == null)
                {
                    throw new Exception("Error obtaining team score.");
                }

                await ReplyAsync(string.Empty,
                                 embed : ScoreEmbedBuilder.CreateTeamDetailsEmbed(teamScore,
                                                                                  completeScoreboard : await ScoreRetrievalService.GetScoreboardAsync(new ScoreboardFilterInfo(null, null)).ConfigureAwait(false),
                                                                                  peerFilter : CompetitionRoundLogicService.GetPeerFilter(ScoreRetrievalService.Round, teamScore.Summary),
                                                                                  timeZone : await Preferences.GetTimeZoneAsync(Context.Guild, Context.User).ConfigureAwait(false)).Build()).ConfigureAwait(false);
            }
        }
        public async Task <ScoreboardDetails> GetDetailsAsync(TeamId team)
        {
            HitTrackingCachedObject <ScoreboardDetails> cachedTeamData = null;
            Func <bool> cachedGoodEnough = () => cachedTeamInformations.TryGetValue(team, out cachedTeamData) && cachedTeamData.Age <= MaxTeamLifespan;

            // FIXME potential undefined/unwanted behavior if reordering dictionary in EnsureTeamCacheCapacity
            // while hitting this branch (it reorders based on HitCount while we alter it, and we don't enter the lock)
            // I dont think this will ever do worse than return a just-removed cache item,
            // or slightly screw up removal sorting (hitcount), but I'm not sure
            // Intended optimization is to avoid hitting the lock if we've got a good enough item cache
            if (cachedGoodEnough())
            {
                // cached info good enough
                Interlocked.Increment(ref cachedTeamData.HitCount);
                return(cachedTeamData.Value);
            }
            else
            {
                await teamCacheLock.WaitAsync().ConfigureAwait(false);

                try
                {
                    // try our read again, but within the lock
                    // if it's succeeded, it means we entered lock in parallel with someone else
                    // but they fixed the problem first
                    if (cachedGoodEnough())
                    {
                        // cached info good enough
                        Interlocked.Increment(ref cachedTeamData.HitCount);
                        return(cachedTeamData.Value);
                    }

                    // cached info either does not exist or is not good enough, and we have exclusive license to write to the cache
                    // make sure we have space
                    // note that this method will write to more than just
                    EnsureTeamCacheCapacity();

                    // pull from backend
                    ScoreboardDetails teamInfo = await Backend.GetDetailsAsync(team).ConfigureAwait(false);

                    // add to cache
                    cachedTeamData = new HitTrackingCachedObject <ScoreboardDetails>(teamInfo);
                    Interlocked.Increment(ref cachedTeamData.HitCount);
                    cachedTeamInformations[team] = cachedTeamData;
                    // return the fresh object
                    // in this case we don't do any fancy wrapping so its ok
                    return(teamInfo);
                }
                finally
                {
                    teamCacheLock.Release();
                }
            }
        }
Пример #4
0
        public async Task GetTeamAsync(TeamId teamId)
        {
            using (Context.Channel.EnterTypingState())
            {
                ScoreboardDetails teamScore = await ScoreRetrievalService.GetDetailsAsync(teamId).ConfigureAwait(false);

                if (teamScore == null)
                {
                    throw new Exception("Error obtaining team score.");
                }
                await ReplyAsync(string.Empty, embed : ScoreEmbedBuilder.CreateTeamDetailsEmbed(teamScore, CompetitionRoundLogicService.GetRankingInformation(ScoreRetrievalService.Round, await ScoreRetrievalService.GetScoreboardAsync(new ScoreboardFilterInfo(teamScore.Summary.Division, null)).ConfigureAwait(false), teamScore.Summary)).Build()).ConfigureAwait(false);
            }
        }
        public async Task GetTeamAsync(TeamId teamId)
        {
            using (Context.Channel.EnterTypingState())
            {
                ScoreboardDetails teamScore = await ScoreRetrievalService.GetDetailsAsync(teamId).ConfigureAwait(false);

                if (teamScore == null)
                {
                    throw new Exception("Error obtaining team score.");
                }
                await ReplyAsync(string.Empty, embed : ScoreEmbedBuilder.CreateTeamDetailsEmbed(teamScore,
                                                                                                completeScoreboard : await ScoreRetrievalService.GetScoreboardAsync(ScoreboardFilterInfo.NoFilter).ConfigureAwait(false),
                                                                                                peerFilter : CompetitionRoundLogicService.GetPeerFilter(ScoreRetrievalService.Round, teamScore.Summary),
                                                                                                timeZone : await Preferences.GetTimeZoneAsync(Context.Guild, Context.User).ConfigureAwait(false)).Build()).ConfigureAwait(false);
            }
        }
        public async Task GeneratePeerLeaderboardAsync(TeamId team)
        {
            using (Context.Channel.EnterTypingState())
            {
                ScoreboardDetails teamDetails = await ScoreRetrievalService.GetDetailsAsync(team).ConfigureAwait(false);

                if (teamDetails == null)
                {
                    throw new Exception("Error obtaining team score.");
                }

                CompleteScoreboardSummary peerScoreboard = await ScoreRetrievalService.GetScoreboardAsync(CompetitionRoundLogicService.GetPeerFilter(ScoreRetrievalService.Round, teamDetails.Summary)).ConfigureAwait(false);

                await ReplyAsync(ScoreEmbedBuilder.CreatePeerLeaderboardEmbed(teamDetails.TeamId, peerScoreboard, timeZone: await Preferences.GetTimeZoneAsync(Context.Guild, Context.User).ConfigureAwait(false))).ConfigureAwait(false);
            }
        }
Пример #7
0
        public async Task GetTeamWithRankAsync(int rank, Division?division = null, Tier?tier = null)
        {
            using (Context.Channel.EnterTypingState())
            {
                if (rank < 1)
                {
                    throw new ArgumentOutOfRangeException(nameof(rank));
                }

                var teams = await ScoreRetrievalService.GetScoreboardAsync(new ScoreboardFilterInfo(division, tier)).ConfigureAwait(false);

                var team = teams.TeamList[rank - 1];
                ScoreboardDetails teamScore = await ScoreRetrievalService.GetDetailsAsync(team.TeamId).ConfigureAwait(false);

                if (teamScore == null)
                {
                    throw new Exception("Error obtaining team score.");
                }
                await ReplyAsync(string.Empty, embed : ScoreEmbedBuilder.CreateTeamDetailsEmbed(teamScore, CompetitionRoundLogicService.GetRankingInformation(ScoreRetrievalService.Round, await ScoreRetrievalService.GetScoreboardAsync(new ScoreboardFilterInfo(teamScore.Summary.Division, null)).ConfigureAwait(false), teamScore.Summary)).Build()).ConfigureAwait(false);
            }
        }
        protected override void ParseDetailedSummaryEntry(ScoreboardDetails details, string[] dataEntries)
        {
            var summary = new ScoreboardSummaryEntry();

            details.Summary = summary;
            // ID, Division (labeled location, their bug), Location (labeled division, their bug), tier, scored img, play time, score time, current score, warn
            summary.TeamId   = TeamId.Parse(dataEntries[0]);
            summary.Category = _categoryProvider?.GetCategory(summary.TeamId);
            if (Utilities.TryParseEnumSpaceless <Division>(dataEntries[1], out Division division))
            {
                summary.Division = division;
            }
            summary.Location = dataEntries[2];
            if (Enum.TryParse <Tier>(dataEntries[3], true, out Tier tier))
            {
                summary.Tier = tier;
            }
            summary.ImageCount = int.Parse(dataEntries[4].Trim());
            summary.PlayTime   = Utilities.ParseHourMinuteTimespan(dataEntries[5]);
            string scoreTimeText = dataEntries[6];
            // to deal with legacy scoreboards
            int scoreTimeIndOffset = 0;

            if (scoreTimeText.Contains(":"))
            {
                details.ScoreTime = Utilities.ParseHourMinuteTimespan(dataEntries[6]);
            }
            else
            {
                details.ScoreTime  = summary.PlayTime;
                scoreTimeIndOffset = -1;
            }
            summary.TotalScore = int.Parse(dataEntries[7 + scoreTimeIndOffset].Trim());
            string warnStr = dataEntries[8 + scoreTimeIndOffset];

            summary.Warnings |= warnStr.Contains("T") ? ScoreWarnings.TimeOver : 0;
            summary.Warnings |= warnStr.Contains("M") ? ScoreWarnings.MultiImage : 0;
        }
        public async Task GetTeamWithRankAsync(int rank, string location, DivisionWithCategory?divisionAndCat, Tier?tier)
        {
            using (Context.Channel.EnterTypingState())
            {
                if (rank < 1)
                {
                    throw new ArgumentOutOfRangeException(nameof(rank));
                }

                var filter = new ScoreboardFilterInfo(divisionAndCat?.Division, tier, divisionAndCat?.Category, location);

                var teams = await ScoreRetrievalService.GetScoreboardAsync(filter).ConfigureAwait(false);

                System.Collections.Generic.IEnumerable <ScoreboardSummaryEntry> teamList = teams.TeamList;


                var team = teamList.Skip(rank - 1).First();
                ScoreboardDetails teamScore = await ScoreRetrievalService.GetDetailsAsync(team.TeamId).ConfigureAwait(false);

                if (teamScore == null)
                {
                    throw new Exception("Error obtaining team score.");
                }

                string classSpec = Utilities.JoinNonNullNonEmpty(", ",
                                                                 LocationResolutionService.GetFullNameOrNull(location),
                                                                 filter.Division.HasValue ? (filter.Division.Value.ToStringCamelCaseToSpace() + " Division") : null,
                                                                 filter.Category?.ToCanonicalName(),
                                                                 tier.HasValue ? (tier.Value.ToStringCamelCaseToSpace() + " Tier") : null);

                await ReplyAsync(
                    "**" + Utilities.AppendOrdinalSuffix(rank) + " place " + (classSpec.Length == 0 ? "overall" : "in " + classSpec) + ": " + team.TeamId + "**",
                    embed : ScoreEmbedBuilder.CreateTeamDetailsEmbed(teamScore,
                                                                     completeScoreboard : await ScoreRetrievalService.GetScoreboardAsync(new ScoreboardFilterInfo(null, null)).ConfigureAwait(false),
                                                                     peerFilter : CompetitionRoundLogicService.GetPeerFilter(ScoreRetrievalService.Round, team),
                                                                     timeZone : await Preferences.GetTimeZoneAsync(Context.Guild, Context.User).ConfigureAwait(false)).Build()).ConfigureAwait(false);
            }
        }
        public async Task <ScoreboardDetails> GetDetailsAsync(TeamId team)
        {
            if (team == null)
            {
                throw new ArgumentNullException(nameof(team));
            }

            string detailsPage;
            Uri    detailsUri = BuildDetailsUri(team);

            await RateLimiter.GetWorkAuthorizationAsync().ConfigureAwait(false);

            Task <string> stringTask = Client.GetStringAsync(detailsUri);

            RateLimiter.AddPrerequisite(stringTask);
            try
            {
                detailsPage = await stringTask.ConfigureAwait(false);

                // hacky, cause they don't return a proper error page for nonexistant teams
                if (!detailsPage.Contains(@"<div id='chart_div' class='chart'>"))
                {
                    throw new ArgumentException("The given team does not exist.");
                }
            }
            catch (HttpRequestException e)
            {
                throw new InvalidOperationException("Error getting team details page, perhaps the scoreboard is offline?", e);
            }

            ScoreboardDetails retVal = new ScoreboardDetails();

            retVal.OriginUri = detailsUri;

            HtmlDocument doc = new HtmlDocument();

            doc.LoadHtml(detailsPage);
            var timestampHeader = doc.DocumentNode.SelectSingleNode("/html/body/div[2]/div/h2[2]")?.InnerText;

            retVal.SnapshotTimestamp = timestampHeader == null ? DateTimeOffset.UtcNow : DateTimeOffset.Parse(timestampHeader.Replace("Generated At: ", string.Empty).Replace("UTC", "+0:00"));
            var summaryHeaderRow     = doc.DocumentNode.SelectSingleNode("/html/body/div[2]/div/table[1]/tr[1]");
            var summaryHeaderRowData = summaryHeaderRow.ChildNodes.Select(x => x.InnerText).ToArray();
            var summaryRow           = doc.DocumentNode.SelectSingleNode("/html/body/div[2]/div/table[1]/tr[2]");
            var summaryRowData       = summaryRow.ChildNodes.Select(x => x.InnerText).ToArray();

            ParseDetailedSummaryEntry(retVal, summaryRowData);

            // summary parsed
            var imagesTable = doc.DocumentNode.SelectSingleNode("/html/body/div[2]/div/table[2]").ChildNodes.Where(n => n.Name != "#text").ToArray();

            for (int i = 1; i < imagesTable.Length; i++)
            {
                // skip team IDs to account for legacy scoreboards
                string[] dataEntries         = imagesTable[i].ChildNodes.Select(n => n.InnerText.Trim()).SkipWhile(s => TeamId.TryParse(s, out TeamId _)).ToArray();
                ScoreboardImageDetails image = new ScoreboardImageDetails();
                image.PointsPossible           = 100;
                image.ImageName                = dataEntries[0];
                image.PlayTime                 = Utilities.ParseHourMinuteSecondTimespan(dataEntries[1]);
                image.VulnerabilitiesFound     = int.Parse(dataEntries[2]);
                image.VulnerabilitiesRemaining = int.Parse(dataEntries[3]);
                image.Penalties                = int.Parse(dataEntries[4]);
                image.Score     = double.Parse(dataEntries[5]);
                image.Warnings |= dataEntries[6].Contains("T") ? ScoreWarnings.TimeOver : 0;
                image.Warnings |= dataEntries[6].Contains("M") ? ScoreWarnings.MultiImage : 0;
                retVal.Images.Add(image);
            }

            // reparse summary table (CCS+Cisco case)
            // pseudoimages: Cisco, administrative adjustment (usually penalty)
            int ciscoIndex   = summaryHeaderRowData.IndexOfWhere(x => x.ToLower().Contains("cisco"));
            int penaltyIndex = summaryHeaderRowData.IndexOfWhere(x => x.ToLower().Contains("adjust"));

            ScoreboardImageDetails CreatePseudoImage(string name, double score, double possible)
            {
                var image = new ScoreboardImageDetails();

                image.PointsPossible = possible;
                image.ImageName      = name;
                image.Score          = score;

                image.VulnerabilitiesFound     = 0;
                image.VulnerabilitiesRemaining = 0;
                image.Penalties = 0;
                image.Warnings  = 0;
                image.PlayTime  = TimeSpan.Zero;

                return(image);
            }

            if (ciscoIndex != -1)
            {
                // pseudoimage
                // FIXME shouldn't display vulns and penalties and time

                double ciscoDenom = -1;
                try
                {
                    ciscoDenom = _roundInferenceService.GetCiscoPointsPossible(Round, retVal.Summary.Division, retVal.Summary.Tier);
                }
                catch
                {
                    // probably because round 0; unknown total
                }

                retVal.Images.Add(CreatePseudoImage("Cisco (Total)", double.Parse(summaryRowData[ciscoIndex]), ciscoDenom));
            }

            if (penaltyIndex != -1)
            {
                retVal.Images.Add(CreatePseudoImage("Administrative Adjustment", double.Parse(summaryRowData[penaltyIndex]), 0));
            }

            // score graph
            try
            {
                var   teamScoreGraphHeader = new Regex(@"\['Time'(?:, '(\w+)')* *\]");
                var   teamScoreGraphEntry  = new Regex(@"\['(\d{2}/\d{2} \d{2}:\d{2})'(?:, (-?\d+|null))*\]");
                Match headerMatch          = teamScoreGraphHeader.Match(detailsPage);
                if (headerMatch?.Success ?? false)
                {
                    retVal.ImageScoresOverTime = new Dictionary <string, SortedDictionary <DateTimeOffset, int?> >();
                    string[] imageHeaders = headerMatch.Groups[1].Captures.Cast <Capture>().Select(c => c.Value).ToArray();
                    SortedDictionary <DateTimeOffset, int?>[] dictArr = new SortedDictionary <DateTimeOffset, int?> [imageHeaders.Length];
                    for (int i = 0; i < dictArr.Length; i++)
                    {
                        dictArr[i] = new SortedDictionary <DateTimeOffset, int?>();
                        retVal.ImageScoresOverTime[imageHeaders[i]] = dictArr[i];
                    }
                    foreach (var m in teamScoreGraphEntry.Matches(detailsPage).Cast <Match>().Where(g => g?.Success ?? false))
                    {
                        DateTimeOffset dto = default(DateTimeOffset);
                        try
                        {
                            // MM/dd hh:mm
                            string   dateStr           = m.Groups[1].Value;
                            string[] dateStrComponents = dateStr.Split(' ');
                            string[] dateComponents    = dateStrComponents[0].Split('/');
                            string[] timeComponents    = dateStrComponents[1].Split(':');
                            dto = new DateTimeOffset(DateTimeOffset.UtcNow.Year, int.Parse(dateComponents[0]), int.Parse(dateComponents[1]), int.Parse(timeComponents[0]), int.Parse(timeComponents[1]), 0, TimeSpan.Zero);
                        }
                        catch
                        {
                            continue;
                        }

                        var captures = m.Groups[2].Captures;

                        for (int i = 0; i < captures.Count; i++)
                        {
                            int?scoreVal = null;
                            if (int.TryParse(captures[i].Value, out int thingValTemp))
                            {
                                scoreVal = thingValTemp;
                            }
                            dictArr[i][dto] = scoreVal;
                        }
                    }
                }
            }
            catch
            {
                // TODO log
            }
            return(retVal);
        }
        public EmbedBuilder CreateTeamDetailsEmbed(ScoreboardDetails teamScore, TeamDetailRankingInformation rankingData = null)
        {
            if (teamScore == null)
            {
                throw new ArgumentNullException(nameof(teamScore));
            }

            var builder = new EmbedBuilder()
                          .WithTimestamp(teamScore.SnapshotTimestamp)
                          .WithTitle("Team " + teamScore.TeamId)
                          .WithDescription(Utilities.JoinNonNullNonEmpty(" | ", CompetitionLogic.GetEffectiveDivisionDescriptor(teamScore.Summary), teamScore.Summary.Tier, teamScore.Summary.Location))
                          .WithFooter(ScoreRetrieverMetadata.StaticSummaryLine);

            if (!string.IsNullOrWhiteSpace(teamScore.Comment))
            {
                builder.Description += "\n";
                builder.Description += teamScore.Comment;
            }

            // scoreboard link
            if (teamScore.OriginUri != null)
            {
                builder.Url = teamScore.OriginUri.ToString();
            }

            // location -> flag in thumbnail
            string flagUrl = FlagProvider.GetFlagUri(teamScore.Summary.Location);

            if (flagUrl != null)
            {
                builder.ThumbnailUrl = flagUrl;
            }

            // tier -> color on side
            // colors borrowed from AFA's spreadsheet
            if (teamScore.Summary.Tier.HasValue)
            {
                switch (teamScore.Summary.Tier.Value)
                {
                case Tier.Platinum:
                    // tweaked from AFA spreadsheet to be more distinct from silver
                    // AFA original is #DAE3F3
                    builder.WithColor(183, 201, 243);
                    break;

                case Tier.Gold:
                    builder.WithColor(0xFF, 0xE6, 0x99);
                    break;

                case Tier.Silver:
                    builder.WithColor(0xF2, 0xF2, 0xF2);
                    break;
                }
            }

            // TODO image lookup for location? e.g. thumbnail with flag?
            foreach (var item in teamScore.Images)
            {
                string       penaltyAppendage = item.Penalties != 0 ? " - " + Utilities.Pluralize("penalty", item.Penalties) : string.Empty;
                bool         overtime         = (item.Warnings & ScoreWarnings.TimeOver) == ScoreWarnings.TimeOver;
                bool         multiimage       = (item.Warnings & ScoreWarnings.MultiImage) == ScoreWarnings.MultiImage;
                string       warningAppendage = string.Empty;
                const string multiImageStr    = "**M**ulti-image";
                const string overTimeStr      = "**T**ime";
                if (overtime || multiimage)
                {
                    warningAppendage = "     Penalties: ";
                }
                if (overtime && multiimage)
                {
                    warningAppendage += multiImageStr + ", " + overTimeStr;
                }
                else if (overtime || multiimage)
                {
                    warningAppendage += multiimage ? multiImageStr : overTimeStr;
                }
                string vulnsString = ScoreFormattingOptions.EvaluateNumericDisplay(ScoreRetrieverMetadata.FormattingOptions.VulnerabilityDisplay, item.VulnerabilitiesFound, item.VulnerabilitiesRemaining + item.VulnerabilitiesFound) ? $" ({item.VulnerabilitiesFound}/{item.VulnerabilitiesFound + item.VulnerabilitiesRemaining} vulns{penaltyAppendage})" : string.Empty;
                string playTimeStr = ScoreFormattingOptions.EvaluateNumericDisplay(ScoreRetrieverMetadata.FormattingOptions.TimeDisplay, item.PlayTime) ? $" in {item.PlayTime:hh\\:mm}" : string.Empty;
                builder.AddField('`' + item.ImageName + $": {ScoreRetrieverMetadata.FormattingOptions.FormatScore(item.Score)}pts`", $"{ScoreRetrieverMetadata.FormattingOptions.FormatScore(item.Score)} points{vulnsString}{playTimeStr}{warningAppendage}");
            }

            string totalScoreTimeAppendage = string.Empty;

            if (ScoreFormattingOptions.EvaluateNumericDisplay(ScoreRetrieverMetadata.FormattingOptions.TimeDisplay, teamScore.Summary.PlayTime))
            {
                totalScoreTimeAppendage = $" in {teamScore.Summary.PlayTime:hh\\:mm}";
            }

            string totalPointsAppendage = string.Empty;

            if (teamScore.Images.All(i => i.PointsPossible != -1))
            {
                totalPointsAppendage =
                    "\n" + ScoreRetrieverMetadata.FormattingOptions.FormatScore(teamScore.Images.Sum(i => i.PointsPossible)) +
                    " points possible";
            }

            builder.AddInlineField("Total Score", $"{ScoreRetrieverMetadata.FormattingOptions.FormatScore(teamScore.Summary.TotalScore)} points" + totalScoreTimeAppendage + totalPointsAppendage);
            if (rankingData != null)
            {
                int myIndexInPeerList = rankingData.PeerIndex;

                double rawPercentile        = 1.0 - ((myIndexInPeerList + 1.0) / rankingData.PeerCount);
                int    multipliedPercentile = (int)Math.Round(rawPercentile * 1000);
                int    intPart   = multipliedPercentile / 10;
                int    floatPart = multipliedPercentile % 10;

                builder.AddInlineField("Rank", $"{Utilities.AppendOrdinalSuffix(myIndexInPeerList + 1)} place\n{(floatPart == 0 ? Utilities.AppendOrdinalSuffix(intPart) : $"{intPart}.{Utilities.AppendOrdinalSuffix(floatPart)}")} percentile");

                StringBuilder marginBuilder = new StringBuilder();
                if (myIndexInPeerList > 0)
                {
                    int marginUnderFirst = rankingData.Peers[0].TotalScore - teamScore.Summary.TotalScore;
                    marginBuilder.AppendLine($"{ScoreRetrieverMetadata.FormattingOptions.FormatLabeledScoreDifference(marginUnderFirst)} under 1st place");
                }
                if (myIndexInPeerList >= 2)
                {
                    int marginUnderAbove = rankingData.Peers[myIndexInPeerList - 1].TotalScore - teamScore.Summary.TotalScore;
                    marginBuilder.AppendLine($"{ScoreRetrieverMetadata.FormattingOptions.FormatLabeledScoreDifference(marginUnderAbove)} under {Utilities.AppendOrdinalSuffix(myIndexInPeerList)} place");
                }
                if (myIndexInPeerList < rankingData.PeerCount - 1)
                {
                    int marginAboveUnder = teamScore.Summary.TotalScore - rankingData.Peers[myIndexInPeerList + 1].TotalScore;
                    marginBuilder.AppendLine($"{ScoreRetrieverMetadata.FormattingOptions.FormatLabeledScoreDifference(marginAboveUnder)} above {Utilities.AppendOrdinalSuffix(myIndexInPeerList + 2)} place");
                }

                // TODO division- and round-specific margins
                builder.AddInlineField("Margin", marginBuilder.ToString());

                StringBuilder standingFieldBuilder = new StringBuilder();
                standingFieldBuilder.AppendLine(Utilities.AppendOrdinalSuffix(myIndexInPeerList + 1) + " of " + Utilities.Pluralize("peer team", rankingData.PeerCount));

                // non-peer rankings use parentheticals - peer rankings are used for the rest of the logic
                // if peer teams != div+tier teams
                if (rankingData.PeerCount != rankingData.TierCount)
                {
                    // tier ranking, differing from peer ranking
                    standingFieldBuilder.AppendLine(Utilities.AppendOrdinalSuffix(rankingData.TierIndex + 1) + " of " + Utilities.Pluralize("team", rankingData.TierCount) + " in tier");
                }
                if (rankingData.PeerCount != rankingData.DivisionCount)
                {
                    // division ranking, differing from peer ranking
                    standingFieldBuilder.AppendLine(Utilities.AppendOrdinalSuffix(rankingData.DivisionIndex + 1) + " of " + Utilities.Pluralize("team", rankingData.DivisionCount) + " in division");
                }
                builder.AddInlineField("Standing", standingFieldBuilder.ToString());
            }
Пример #12
0
        public EmbedBuilder CreateTeamDetailsEmbed(ScoreboardDetails teamScore, CompleteScoreboardSummary completeScoreboard = null, ScoreboardFilterInfo peerFilter = default(ScoreboardFilterInfo), TimeZoneInfo timeZone = null)
        {
            if (teamScore == null)
            {
                throw new ArgumentNullException(nameof(teamScore));
            }

            var builder = new EmbedBuilder()
                          .WithTimestamp(teamScore.SnapshotTimestamp)
                          .WithTitle("Team " + teamScore.TeamId)
                          .WithDescription(Utilities.JoinNonNullNonEmpty(" | ", CompetitionLogic.GetEffectiveDivisionDescriptor(teamScore.Summary), teamScore.Summary.Tier, LocationResolution.GetFullName(teamScore.Summary.Location)))
                          .WithFooter(ScoreRetrieverMetadata.StaticSummaryLine);

            if (!string.IsNullOrWhiteSpace(teamScore.Comment))
            {
                builder.Description += "\n";
                builder.Description += teamScore.Comment;
            }

            // scoreboard link
            if (teamScore.OriginUri != null)
            {
                builder.Url = teamScore.OriginUri.ToString();
            }

            // location -> flag in thumbnail
            Uri flagUrl = LocationResolution?.GetFlagUriOrNull(teamScore.Summary.Location);

            if (flagUrl != null)
            {
                builder.ThumbnailUrl = flagUrl.ToString();
            }

            // tier -> color on side
            // colors borrowed from AFA's spreadsheet
            if (teamScore.Summary.Tier.HasValue)
            {
                switch (teamScore.Summary.Tier.Value)
                {
                case Tier.Platinum:
                    // tweaked from AFA spreadsheet to be more distinct from silver
                    // AFA original is #DAE3F3
                    builder.WithColor(183, 201, 243);
                    break;

                case Tier.Gold:
                    builder.WithColor(0xFF, 0xE6, 0x99);
                    break;

                case Tier.Silver:
                    // tweaked from AFA spreadsheet to be more distinct from platinum, and to look less white
                    // AFA original is #F2F2F2
                    builder.WithColor(0x90, 0x90, 0x90);
                    break;
                }
            }

            // TODO image lookup for location? e.g. thumbnail with flag?
            foreach (var item in teamScore.Images)
            {
                string       penaltyAppendage = item.Penalties != 0 ? " - " + Utilities.Pluralize("penalty", item.Penalties) : string.Empty;
                bool         overtime         = (item.Warnings & ScoreWarnings.TimeOver) == ScoreWarnings.TimeOver;
                bool         multiimage       = (item.Warnings & ScoreWarnings.MultiImage) == ScoreWarnings.MultiImage;
                string       warningAppendage = string.Empty;
                const string multiImageStr    = "**M**ulti-instance";
                const string overTimeStr      = "**T**ime";
                if (overtime || multiimage)
                {
                    warningAppendage = "\nWarnings: ";
                }
                if (overtime && multiimage)
                {
                    warningAppendage += multiImageStr + ", " + overTimeStr;
                }
                else if (overtime || multiimage)
                {
                    warningAppendage += multiimage ? multiImageStr : overTimeStr;
                }
                string vulnsString = ScoreFormattingOptions.EvaluateNumericDisplay(ScoreRetrieverMetadata.FormattingOptions.VulnerabilityDisplay, item.VulnerabilitiesFound, item.VulnerabilitiesRemaining + item.VulnerabilitiesFound) ? $" ({item.VulnerabilitiesFound}/{item.VulnerabilitiesFound + item.VulnerabilitiesRemaining} vulns{penaltyAppendage})" : string.Empty;
                string playTimeStr = ScoreFormattingOptions.EvaluateNumericDisplay(ScoreRetrieverMetadata.FormattingOptions.TimeDisplay, item.PlayTime) ? $" in {item.PlayTime.ToHoursMinutesSecondsString()}" : string.Empty;
                builder.AddField('`' + item.ImageName + $": {ScoreRetrieverMetadata.FormattingOptions.FormatScore(item.Score)}pts`", $"{ScoreRetrieverMetadata.FormattingOptions.FormatScore(item.Score)} points{vulnsString}{playTimeStr}{warningAppendage}");
            }

            string totalScoreTimeAppendage = string.Empty;

            if (ScoreFormattingOptions.EvaluateNumericDisplay(ScoreRetrieverMetadata.FormattingOptions.TimeDisplay, teamScore.Summary.PlayTime))
            {
                totalScoreTimeAppendage = $" in {teamScore.Summary.PlayTime.ToHoursMinutesSecondsString()}";
            }

            string totalPointsAppendage = string.Empty;

            if (teamScore.Images.All(i => i.PointsPossible != -1))
            {
                totalPointsAppendage =
                    "\n" + ScoreRetrieverMetadata.FormattingOptions.FormatScore(teamScore.Images.Sum(i => i.PointsPossible)) +
                    " points possible";
            }

            builder.AddInlineField("Total Score", $"{ScoreRetrieverMetadata.FormattingOptions.FormatScore(teamScore.Summary.TotalScore)} points" + totalScoreTimeAppendage + totalPointsAppendage);

            if (teamScore.Summary.Warnings != 0)
            {
                string warningsOverview = null;
                if ((teamScore.Summary.Warnings & ScoreWarnings.MultiImage) == ScoreWarnings.MultiImage)
                {
                    warningsOverview = "Multiple Instances";
                }

                if ((teamScore.Summary.Warnings & ScoreWarnings.TimeOver) == ScoreWarnings.TimeOver)
                {
                    if (warningsOverview == null)
                    {
                        warningsOverview = "";
                    }
                    else
                    {
                        warningsOverview += "\n";
                    }

                    warningsOverview += "Time Limit Exceeded";
                }

                builder.AddInlineField("Warnings", warningsOverview);
            }

            var timingFieldBuilder = new StringBuilder();

            if (ScoreFormattingOptions.EvaluateNumericDisplay(ScoreRetrieverMetadata.FormattingOptions.TimeDisplay, teamScore.ScoreTime))
            {
                if (timingFieldBuilder.Length > 0)
                {
                    timingFieldBuilder.AppendLine();
                }
                timingFieldBuilder.AppendFormat("Score achieved in {0}", teamScore.ScoreTime.ToHoursMinutesSecondsString());
            }

            DateTimeOffset?maxImageTime = null;

            if (teamScore.ImageScoresOverTime != null)
            {
                foreach (var dto in teamScore.ImageScoresOverTime.Select(x => x.Value.Keys.Last()))
                {
                    if (!maxImageTime.HasValue || dto > maxImageTime.Value)
                    {
                        maxImageTime = dto;
                    }
                }
            }

            if (maxImageTime.HasValue)
            {
                if (timingFieldBuilder.Length > 0)
                {
                    timingFieldBuilder.AppendLine();
                }
                timingFieldBuilder.Append("Score last updated:");
                timingFieldBuilder.AppendLine().Append("\u00A0\u00A0"); //NBSP x2

                if (DateTimeOffset.UtcNow - maxImageTime.Value < TimeSpan.FromDays(1))
                {
                    timingFieldBuilder.Append((DateTimeOffset.UtcNow - maxImageTime.Value).ToLongString(showSeconds: false)).Append(" ago");
                }
                else
                {
                    DateTimeOffset timestamp = timeZone == null ? maxImageTime.Value : TimeZoneInfo.ConvertTime(maxImageTime.Value, timeZone);
                    timingFieldBuilder.AppendFormat("{0:g} ", timestamp);
                    timingFieldBuilder.Append(TimeZoneNames.TZNames.GetAbbreviationsForTimeZone(timeZone.Id, "en-US").Generic.Replace("UTC Time", "UTC"));
                }
            }

            if (timingFieldBuilder.Length > 0)
            {
                builder.AddInlineField("Timing", timingFieldBuilder.ToString());
            }

            if (completeScoreboard != null)
            {
                var peerList          = completeScoreboard.Clone().WithFilter(peerFilter).TeamList;
                int myIndexInPeerList = peerList.IndexOfWhere(x => x.TeamId == teamScore.TeamId);

                double rawPercentile        = 1.0 - ((myIndexInPeerList + 1.0) / peerList.Count);
                int    multipliedPercentile = (int)Math.Round(rawPercentile * 1000);
                int    intPart   = multipliedPercentile / 10;
                int    floatPart = multipliedPercentile % 10;

                builder.AddInlineField("Rank", $"{Utilities.AppendOrdinalSuffix(myIndexInPeerList + 1)} place\n{(floatPart == 0 ? Utilities.AppendOrdinalSuffix(intPart) : $"{intPart}.{Utilities.AppendOrdinalSuffix(floatPart)}")} percentile");

                StringBuilder marginBuilder = new StringBuilder();
                if (myIndexInPeerList > 0)
                {
                    double marginUnderFirst = peerList[0].TotalScore - teamScore.Summary.TotalScore;
                    marginBuilder.AppendLine($"{ScoreRetrieverMetadata.FormattingOptions.FormatLabeledScoreDifference(marginUnderFirst)} under 1st place");
                }
                if (myIndexInPeerList >= 2)
                {
                    double marginUnderAbove = peerList[myIndexInPeerList - 1].TotalScore - teamScore.Summary.TotalScore;
                    marginBuilder.AppendLine($"{ScoreRetrieverMetadata.FormattingOptions.FormatLabeledScoreDifference(marginUnderAbove)} under {Utilities.AppendOrdinalSuffix(myIndexInPeerList)} place");
                }
                if (myIndexInPeerList < peerList.Count - 1)
                {
                    double marginAboveUnder = teamScore.Summary.TotalScore - peerList[myIndexInPeerList + 1].TotalScore;
                    marginBuilder.AppendLine($"{ScoreRetrieverMetadata.FormattingOptions.FormatLabeledScoreDifference(marginAboveUnder)} above {Utilities.AppendOrdinalSuffix(myIndexInPeerList + 2)} place");
                }

                // TODO division- and round-specific margins
                builder.AddInlineField("Margin", marginBuilder.ToString());

                StringBuilder standingFieldBuilder = new StringBuilder();

                IList <ScoreboardSummaryEntry> subPeer = null;
                string subPeerLabel = null;

                if (!peerFilter.Category.HasValue && teamScore.Summary.Category.HasValue)
                {
                    var myCategory = teamScore.Summary.Category.Value;
                    subPeer      = peerList.Where(x => x.Category == myCategory).ToIList();
                    subPeerLabel = " in category";
                }
                else if (peerFilter.Location == null && teamScore.Summary.Location != null)
                {
                    var myLocation = teamScore.Summary.Location;
                    subPeer      = peerList.Where(x => x.Location == myLocation).ToIList();
                    subPeerLabel = " in state";
                }

                if (subPeerLabel != null)
                {
                    standingFieldBuilder.AppendLine(Utilities.AppendOrdinalSuffix(subPeer.IndexOfWhere(x => x.TeamId == teamScore.TeamId) + 1) + " of " + Utilities.Pluralize("peer team", subPeer.Count) + subPeerLabel);
                }

                standingFieldBuilder.AppendLine(Utilities.AppendOrdinalSuffix(myIndexInPeerList + 1) + " of " + Utilities.Pluralize("peer team", peerList.Count));

                // if peer teams != div+tier teams
                if ((peerFilter.Category.HasValue || peerFilter.Location != null) && peerFilter.Tier.HasValue)
                {
                    // tier ranking, differing from peer ranking
                    var tierTeams = completeScoreboard.Clone().WithFilter(new ScoreboardFilterInfo(peerFilter.Division, peerFilter.Tier)).TeamList;
                    standingFieldBuilder.AppendLine(Utilities.AppendOrdinalSuffix(tierTeams.IndexOfWhere(x => x.TeamId == teamScore.TeamId) + 1) + " of " + Utilities.Pluralize("team", tierTeams.Count) + " in tier");
                }
                if (peerFilter.Category.HasValue || peerFilter.Location != null || peerFilter.Tier.HasValue)
                {
                    // division ranking, differing from peer ranking
                    var divTeams = completeScoreboard.Clone().WithFilter(new ScoreboardFilterInfo(peerFilter.Division, null)).TeamList;
                    standingFieldBuilder.AppendLine(Utilities.AppendOrdinalSuffix(divTeams.IndexOfWhere(x => x.TeamId == teamScore.TeamId) + 1) + " of " + Utilities.Pluralize("team", divTeams.Count) + " in division");
                }
                builder.AddInlineField("Standing", standingFieldBuilder.ToString());
            }