/// <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(); } } }
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); } }
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()); }
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()); }