Skip to content

Commit

Permalink
Add player profile viewer
Browse files Browse the repository at this point in the history
  • Loading branch information
Simyon264 committed Apr 29, 2024
1 parent 5f90d76 commit 28071fe
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 5 deletions.
7 changes: 4 additions & 3 deletions Client/Components/Pages/Leaderboard.razor
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ else
@foreach (var player in LeaderboardData.MostSeenPlayers)
{
<tr>
<td>@player.Value.Player.Username</td>
// Player name with link to their profile
<td><a href="/player/@player.Value.Player.PlayerGuid">@player.Value.Player.Username</a></td>
<td>@player.Value.Count</td>
</tr>
}
Expand All @@ -51,7 +52,7 @@ else
@foreach (var player in LeaderboardData.MostAntagPlayers)
{
<tr>
<td>@player.Value.Player.Username</td>
<td><a href="/player/@player.Value.Player.PlayerGuid">@player.Value.Player.Username</a></td>
<td>@player.Value.Count</td>
</tr>
}
Expand All @@ -69,7 +70,7 @@ else
@foreach (var player in LeaderboardData.MostHuntedPlayer)
{
<tr>
<td>@player.Value.Player.Username</td>
<td><a href="/player/@player.Value.Player.PlayerGuid">@player.Value.Player.Username</a></td>
<td>@player.Value.Count</td>
</tr>
}
Expand Down
68 changes: 68 additions & 0 deletions Client/Components/Pages/Player.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
@page "/player/{guid}"
@using System.ComponentModel
@using Humanizer
@using Shared
@inject HttpClient Http
@inject NavigationManager NavigationManager
@attribute [StreamRendering]

<PageTitle>Player info</PageTitle>

@if (playerData is null)
{
<p>Loading...</p>
}
else
{
<h1>@playerData.PlayerData.Username</h1>

<p><b>Total estimated playtime:</b> @playerData.TotalEstimatedPlaytime.Humanize()</p>
<p><small>Calculated by adding up the total time for each round played by the player.</small></p>
<p><b>Total rounds played:</b> @playerData.TotalRoundsPlayed</p>
<p><b>Total antag rounds played:</b> @playerData.TotalAntagRoundsPlayed</p>
<p><b>Last seen:</b> @playerData.LastSeen.Humanize()</p>

<h2>Characters</h2>
<table class="table">
<thead>
<tr>
<th>Character Name</th>
<th>Times played</th>
<th>Last played</th>
</tr>
</thead>
<tbody>
@foreach (var character in playerData.Characters)
{
if (character.CharacterName == "Unknown")
{
continue;
}

// If this character contains a paranthesis, it's likely a ghost role like mouse (253), so we'll skip it.
if (character.CharacterName.Contains("("))
{
continue;
}

<tr>
<td>@character.CharacterName</td>
<td>@character.RoundsPlayed</td>
<td>@character.LastPlayed.Humanize()</td>
</tr>
}
</tbody>
</table>
}

@code{
[Parameter] public string Guid { get; set; } = string.Empty;

private CollectedPlayerData? playerData;

protected override async Task OnInitializedAsync()
{
playerData = await Http.GetFromJsonAsync<CollectedPlayerData>($"api/Data/player-data?guid={Guid}");
playerData.Characters = playerData.Characters.OrderByDescending(x => x.RoundsPlayed).ToList();
}
}
4 changes: 2 additions & 2 deletions Client/Components/ReplayViewer.razor
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,10 @@
job = player.JobPrototypes[0];
}

var playerText = $"<span style=\"color: gray\">{player.PlayerOocName}</span> was <bold>{player.PlayerIcName}</bold> playing role of <span style=\"color: orange\"><bold>{job}</bold></span>";
var playerText = $"<a href=\"/player/{player.PlayerGuid}\"><span style=\"color: gray\">{player.PlayerOocName}</span></a> was <bold>{player.PlayerIcName}</bold> playing role of <span style=\"color: orange\"><bold>{job}</bold></span>";
if (player.AntagPrototypes.Count > 0)
{
playerText = $"<span style=\"color: gray\">{player.PlayerOocName}</span> was <span style=\"color:red\"><bold>{player.PlayerIcName}</bold></span> playing role of <span style=\"color: orange\"><bold>{job}</bold></span>";
playerText = $"<a href=\"/player/{player.PlayerGuid}\"><span style=\"color: red\">{player.PlayerOocName}</span></a> was <span style=\"color:red\"><bold>{player.PlayerIcName}</bold></span> playing role of <span style=\"color: orange\"><bold>{job}</bold></span>";
}
// Need to show the guid as well
playerText += $"<br><span style=\"color: gray;font-size: x-small;\"> {{{player.PlayerGuid}}}</span>";
Expand Down
102 changes: 102 additions & 0 deletions Server/Api/DataController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,108 @@ public DataController(ReplayDbContext context, IMemoryCache cache)
_cache = cache;
}

[HttpGet]
[Route("player-data")]
public async Task<ActionResult> GetPlayerData(
[FromQuery] string guid
)
{
var playerGuid = Guid.Parse(guid);
if (playerGuid == Guid.Empty)
{
return BadRequest("Invalid GUID");
}

var replays = await _context.Players
.Where(p => p.PlayerGuid == playerGuid)
.Include(p => p.Replay)
.Include(r => r.Replay.RoundEndPlayers)

Check warning on line 54 in Server/Api/DataController.cs

View workflow job for this annotation

GitHub Actions / deploy

Dereference of a possibly null reference.
.Select(p => p.Replay)
.ToListAsync();

var charactersPlayed = new List<CharacterData>();
var totalPlaytime = TimeSpan.Zero;
var totalRoundsPlayed = 0;
var totalAntagRoundsPlayed = 0;
var lastSeen = DateTime.MinValue;

foreach (var replay in replays)
{
if (replay == null)
{
Log.Warning("Replay is null for player with GUID {PlayerGuid}", playerGuid);
continue;
}

if (replay.RoundEndPlayers == null)
continue;

if (replay.Date > lastSeen) // Update last seen
{
lastSeen = (DateTime)replay.Date;
}

var characters = replay.RoundEndPlayers
.Where(p => p.PlayerGuid == playerGuid)
.Select(p => p.PlayerIcName)
.Distinct()
.ToList();

foreach (var character in characters)
{
// Check if the character is already in the list
var characterData = charactersPlayed.FirstOrDefault(c => c.CharacterName == character);
if (characterData == null)
{
charactersPlayed.Add(new CharacterData()
{
CharacterName = character,
LastPlayed = (DateTime)replay.Date,

Check warning on line 95 in Server/Api/DataController.cs

View workflow job for this annotation

GitHub Actions / deploy

Nullable value type may be null.
RoundsPlayed = 1
});
}
else
{
characterData.RoundsPlayed++;
if (replay.Date > characterData.LastPlayed)
{
characterData.LastPlayed = (DateTime)replay.Date;
}
}
}

// Since duration is a string (example 02:04:51.4258419), we need to parse it.
if (TimeSpan.TryParse(replay.Duration, out var duration))
{
totalPlaytime += duration;
}
else
{
Log.Warning("Unable to parse duration {Duration} for replay with ID {ReplayId}", replay.Duration, replay.Id);
}

totalRoundsPlayed++;
totalAntagRoundsPlayed += replay.RoundEndPlayers.Any(p => p.PlayerGuid == playerGuid && p.Antag) ? 1 : 0; // If the player is an antag, increment the count.
}

CollectedPlayerData collectedPlayerData = new()
{
PlayerData = new PlayerData()
{
PlayerGuid = playerGuid,
Username = (await FetchPlayerDataFromGuid(playerGuid))?.Username ??
"Unable to fetch username (API error)"
},
Characters = charactersPlayed,
TotalEstimatedPlaytime = totalPlaytime,
TotalRoundsPlayed = totalRoundsPlayed,
TotalAntagRoundsPlayed = totalAntagRoundsPlayed,
LastSeen = lastSeen
};

return Ok(collectedPlayerData);
}

/// <summary>
/// Provides a list of usernames which start with the given username.
/// </summary>
Expand Down
53 changes: 53 additions & 0 deletions Shared/CollectedPlayerData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
namespace Shared;

/// <summary>
/// Represents a player's data over all replays.
/// </summary>
public class CollectedPlayerData
{
public PlayerData PlayerData { get; init; } = new();

/// <summary>
/// Characters played by the player
/// </summary>
public List<CharacterData> Characters { get; set; } = new();

/// <summary>
/// Represents the estimated total playtime of the player. This is calculated by summing the roundtime of all replays the player has played.
/// </summary>
public TimeSpan TotalEstimatedPlaytime { get; init; }

/// <summary>
/// Represents the total amount of rounds the player has played.
/// </summary>
public int TotalRoundsPlayed { get; init; }

/// <summary>
/// Represents the total amount of antag rounds the player has played.
/// </summary>
public int TotalAntagRoundsPlayed { get; init; }

public DateTime LastSeen { get; set; } = DateTime.MinValue;

public override bool Equals(object? obj)
{
if (obj is not CollectedPlayerData other)
{
return false;
}

return PlayerData.Equals(other.PlayerData);
}

public override int GetHashCode()
{
return PlayerData.GetHashCode();
}
}

public class CharacterData
{
public string CharacterName { get; init; }

Check warning on line 50 in Shared/CollectedPlayerData.cs

View workflow job for this annotation

GitHub Actions / deploy

Non-nullable property 'CharacterName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public DateTime LastPlayed { get; set; } = DateTime.MinValue;
public int RoundsPlayed { get; set; }
}

0 comments on commit 28071fe

Please sign in to comment.