diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 7fccd84..f008c3d 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -21,6 +21,7 @@ env: ATLAS_CHART_ID_EVENT_PROD: ${{ secrets.ATLAS_CHART_ID_EVENT_PROD }} ATLAS_CHART_ID_PLAYER_PROD: ${{ secrets.ATLAS_CHART_ID_PLAYER_PROD }} ATLAS_CHART_ID_HOME_PROD: ${{ secrets.ATLAS_CHART_ID_HOME_PROD }} + ATLAS_CHART_ID_SIMILAR_PROD: ${{ secrets.ATLAS_CHART_ID_SIMILAR_PROD }} CONNECTION_STRING_STAGING: ${{ secrets.CONNECTION_STRING_STAGING }} REST_SERVICE_IP_STAGING: ${{ secrets.REST_SERVICE_IP_STAGING }} @@ -29,6 +30,7 @@ env: ATLAS_CHART_ID_EVENT_STAGING: ${{ secrets.ATLAS_CHART_ID_EVENT_STAGING }} ATLAS_CHART_ID_PLAYER_STAGING: ${{ secrets.ATLAS_CHART_ID_PLAYER_STAGING}} ATLAS_CHART_ID_HOME_STAGING: ${{ secrets.ATLAS_CHART_ID_HOME_STAGING }} + ATLAS_CHART_ID_SIMILAR_STAGING: ${{ secrets.ATLAS_CHART_ID_SIMILAR_STAGING }} jobs: deployment: @@ -49,6 +51,7 @@ jobs: echo "ATLAS_CHART_ID_EVENT=${ATLAS_CHART_ID_EVENT_STAGING}" >> $GITHUB_ENV echo "ATLAS_CHART_ID_PLAYER=${ATLAS_CHART_ID_PLAYER_STAGING}" >> $GITHUB_ENV echo "ATLAS_CHART_ID_HOME=${ATLAS_CHART_ID_HOME_STAGING}" >> $GITHUB_ENV + echo "ATLAS_CHART_ID_SIMILAR=${ATLAS_CHART_ID_SIMILAR_STAGING}" >> $GITHUB_ENV echo "GAME_CLIENT_PORT=${GAME_CLIENT_PORT}" >> $GITHUB_ENV @@ -63,6 +66,7 @@ jobs: echo "ATLAS_CHART_ID_EVENT=${ATLAS_CHART_ID_EVENT_PROD}" >> $GITHUB_ENV echo "ATLAS_CHART_ID_PLAYER=${ATLAS_CHART_ID_PLAYER_PROD}" >> $GITHUB_ENV echo "ATLAS_CHART_ID_HOME=${ATLAS_CHART_ID_HOME_PROD}" >> $GITHUB_ENV + echo "ATLAS_CHART_ID_SIMILAR=${ATLAS_CHART_ID_SIMILAR_PROD}" >> $GITHUB_ENV echo "GAME_CLIENT_PORT=${GAME_CLIENT_PORT}" >> $GITHUB_ENV @@ -116,6 +120,7 @@ jobs: echo "ATLAS_CHART_ID_PLAYER=${{ env.ATLAS_CHART_ID_PLAYER }}" >> ./website/.env echo "ATLAS_CHART_ID_HOME=${{ env.ATLAS_CHART_ID_HOME }}" >> ./website/.env echo "ATLAS_CHART_ID_HOME=${{ env.ATLAS_CHART_ID_HOME }}" >> ./website/.env + echo "ATLAS_CHART_ID_SIMILAR=${{ env.ATLAS_CHART_ID_SIMILAR }}" >> ./website/.env echo "GAME_CLIENT_PORT=${{ env.GAME_CLIENT_PORT }}" >> ./website/.env diff --git a/deployment/game_database/vector_index.js b/deployment/game_database/vector_index.js new file mode 100644 index 0000000..4c3481c --- /dev/null +++ b/deployment/game_database/vector_index.js @@ -0,0 +1,26 @@ +// Create the following atlas vector search index: + +// ========================== +// named: vector_index +// collection: recordings +// ========================== +{ + "fields": [ + { + "path": "Player.Nickname", + "type": "filter" + }, + { + "numDimensions": 589, + "path": "speed_vector", + "similarity": "euclidean", + "type": "vector" + }, + { + "numDimensions": 588, + "path": "accel_vector", + "similarity": "euclidean", + "type": "vector" + } + ] +} \ No newline at end of file diff --git a/rest_service/Controllers/RecordingsController.cs b/rest_service/Controllers/RecordingsController.cs index 8654eb6..8ee7b81 100644 --- a/rest_service/Controllers/RecordingsController.cs +++ b/rest_service/Controllers/RecordingsController.cs @@ -1,8 +1,11 @@ using Microsoft.AspNetCore.Mvc; using MongoDB.Driver; using RestService.Dtos.RequestObjects; +using RestService.Dtos.ResponseObjects; using RestService.Entities; using RestService.Exceptions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; namespace RestService.Controllers; @@ -38,6 +41,7 @@ public async Task PostRecording([FromBody] RecordingRequest recor var newRecording = new Recording() { + Id = ObjectId.GenerateNewId(), SessionStatisticsPlain = recordingRequest.SessionStatisticsPlain, DateTime = DateTime.UtcNow, Player = new RecordingPlayer { Name = recordingRequest.PlayerName }, @@ -53,6 +57,15 @@ public async Task PostRecording([FromBody] RecordingRequest recor }).ToList() }; + // Calculate vectors + try + { + newRecording.SpeedVector = CalculateSpeedVector(newRecording.Snapshots); + newRecording.AccelVector = CalculateAcceleration(newRecording.SpeedVector); + } catch (Exception) { + // Favor persisting Recording over setting vectors + } + try { await AddLocation(newRecording); @@ -129,4 +142,113 @@ private async Task AddPlayer(Recording recording) throw new MultiplePlayersFoundException(); } } + + private static double[] CalculateSpeedVector(List snapshots) + { + long vectorSize = snapshots.Count - 1; + if (vectorSize != 589) // 590 represents movements in 60s + return Array.Empty(); + + double[] speed = new double[vectorSize]; + for (int i = 0; i < vectorSize; i++) + { + double speedX = snapshots[i + 1].Position.X - snapshots[i].Position.X; + //double Y = snapshots[i + 1].Position.X - snapshots[i].Position.X; + double speedY = snapshots[i + 1].Position.Z - snapshots[i].Position.Z; + speed[i] = Math.Sqrt(Math.Pow(speedX, 2) + Math.Pow(speedY, 2)); + } + + return speed; + } + + private static double[] CalculateAcceleration(double[] speedVector) + { + long vectorSize = speedVector.Length - 1; + if (speedVector.Length != 589) // speed vector should be 1 less 60s run + return Array.Empty(); + + double dt = 1; // Assuming a constant time step of 1 unit. + double[] accelVector = new double[vectorSize]; + + for (int i = 0; i < speedVector.Length - 1; i++) + accelVector[i] = (speedVector[i + 1] - speedVector[i]) / dt; + + return accelVector; + } + + [HttpGet("similarBySpeed", Name = "GetSimilarBySpeed")] + public async Task> SimilarBySpeed([FromQuery] PlayerRequest playerRequest) + { + // Get the highest scoring run for this player + Recording topRecording = _recordingsCollection + .Find(r => r.Player.Name.Equals(playerRequest.Name)) + .SortByDescending(r => r.SessionStatisticsPlain.Score) + .Limit(1).ToList().First(); + + // Now get similar recordings + List similarRecordings = _recordingsCollection.Aggregate() + .VectorSearch( + r => r.SpeedVector, + topRecording.SpeedVector, + 3, + new VectorSearchOptions() + { + IndexName = "vector_index", + NumberOfCandidates = 1000, + Filter = Builders.Filter + .Where(r => !r.Player.Name.Equals(playerRequest.Name)) + }) + .ToList(); + + // Return this player's top recording + top similar + List response = new() + { + new SimilarRecordingResponse(topRecording) + }; + response.AddRange( + similarRecordings + .Select(r => new SimilarRecordingResponse(r)) + .ToList()); + + return response; + } + + + [HttpGet("similarByAcceleration", Name = "GetSimilarByAcceleration")] + public async Task> SimilarByAcceleration([FromQuery] PlayerRequest playerRequest) + { + // Get the highest scoring run for this player + Recording topRecording = _recordingsCollection + .Find(r => r.Player.Name.Equals(playerRequest.Name)) + .SortByDescending(r => r.SessionStatisticsPlain.Score) + .Limit(1).ToList().First(); + + // Now get similar recordings + List similarRecordings = _recordingsCollection.Aggregate() + .VectorSearch( + r => r.AccelVector, + topRecording.AccelVector, + 3, + new VectorSearchOptions() + { + IndexName = "vector_index", + NumberOfCandidates = 1000, + Filter = Builders.Filter + .Where(r => !r.Player.Name.Equals(playerRequest.Name)) + }) + .ToList(); + + // Return this player's top recording + top similar + List response = new() + { + new SimilarRecordingResponse(topRecording) + }; + response.AddRange( + similarRecordings + .Select(r => new SimilarRecordingResponse(r)) + .ToList()); + + return response; + } + } \ No newline at end of file diff --git a/rest_service/Dtos/RequestObjects/RecordingRequest.cs b/rest_service/Dtos/RequestObjects/RecordingRequest.cs index 74f51aa..3fb0057 100644 --- a/rest_service/Dtos/RequestObjects/RecordingRequest.cs +++ b/rest_service/Dtos/RequestObjects/RecordingRequest.cs @@ -1,10 +1,7 @@ -using System.Diagnostics.CodeAnalysis; -using RestService.Entities; +using RestService.Entities; namespace RestService.Dtos.RequestObjects; -[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] -[SuppressMessage("ReSharper", "CollectionNeverUpdated.Global")] public class RecordingRequest { public SessionStatisticsPlain? SessionStatisticsPlain { get; set; } diff --git a/rest_service/Dtos/RequestObjects/SnapshotRequest.cs b/rest_service/Dtos/RequestObjects/SnapshotRequest.cs index fceaf0e..f71fe43 100644 --- a/rest_service/Dtos/RequestObjects/SnapshotRequest.cs +++ b/rest_service/Dtos/RequestObjects/SnapshotRequest.cs @@ -1,10 +1,7 @@ -using System.Diagnostics.CodeAnalysis; using RestService.Entities; namespace RestService.Dtos.RequestObjects; -[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] -[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] public class SnapshotRequest { public Position? Position { get; set; } diff --git a/rest_service/Dtos/ResponseObjects/ConfigResponse.cs b/rest_service/Dtos/ResponseObjects/ConfigResponse.cs index fe26cf8..08db688 100644 --- a/rest_service/Dtos/ResponseObjects/ConfigResponse.cs +++ b/rest_service/Dtos/ResponseObjects/ConfigResponse.cs @@ -3,7 +3,6 @@ namespace RestService.Dtos.ResponseObjects; -[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] public class ConfigResponse { public float RoundDuration { get; set; } diff --git a/rest_service/Dtos/ResponseObjects/PlayerResponse.cs b/rest_service/Dtos/ResponseObjects/PlayerResponse.cs index 0301465..1ec4a40 100644 --- a/rest_service/Dtos/ResponseObjects/PlayerResponse.cs +++ b/rest_service/Dtos/ResponseObjects/PlayerResponse.cs @@ -1,11 +1,8 @@ -using System.Diagnostics.CodeAnalysis; -using MongoDB.Bson.Serialization.Attributes; using Newtonsoft.Json; using RestService.Entities; namespace RestService.Dtos.ResponseObjects; -[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] public class PlayerResponse { [JsonProperty("_id")] public string? Id { get; set; } @@ -22,13 +19,4 @@ public PlayerResponse(Player player) Team = player.Team; Location = player.Location; } - - /* - public PlayerResponse(PlayerUnique player) - { - Id = player.Id.ToString(); - Name = player.Name; - Location = player.Location; - } - */ } \ No newline at end of file diff --git a/rest_service/Dtos/ResponseObjects/SimilarRecordingResponse.cs b/rest_service/Dtos/ResponseObjects/SimilarRecordingResponse.cs new file mode 100644 index 0000000..50b66f9 --- /dev/null +++ b/rest_service/Dtos/ResponseObjects/SimilarRecordingResponse.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; +using RestService.Entities; + +namespace RestService.Dtos.ResponseObjects; + +public class SimilarRecordingResponse +{ + [JsonProperty("_id")] + public string? Id { get; set; } + [JsonProperty("sessionStatisticsPlain")] + public SessionStatisticsPlain? SessionStatisticsPlain { get; set; } + [JsonProperty("name")] + public string? Name { get; set; } + + public SimilarRecordingResponse(Recording recording) + { + Id = recording.Id.ToString(); + SessionStatisticsPlain = recording.SessionStatisticsPlain; + Name = recording.Player.Name; + } +} + diff --git a/rest_service/Entities/Config.cs b/rest_service/Entities/Config.cs index 3886ca7..2e30ed7 100644 --- a/rest_service/Entities/Config.cs +++ b/rest_service/Entities/Config.cs @@ -1,11 +1,9 @@ -using System.Diagnostics.CodeAnalysis; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; namespace RestService.Entities; -[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] -[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] +[BsonIgnoreExtraElements] public class Config { [BsonElement("_id")] public ObjectId? Id { get; set; } diff --git a/rest_service/Entities/Event.cs b/rest_service/Entities/Event.cs index 306fe1c..ba102b0 100644 --- a/rest_service/Entities/Event.cs +++ b/rest_service/Entities/Event.cs @@ -1,10 +1,8 @@ -using System.Diagnostics.CodeAnalysis; using MongoDB.Bson.Serialization.Attributes; namespace RestService.Entities; -[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] -[SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] +[BsonIgnoreExtraElements] public class Event { [BsonElement("_id")] public string? Id { get; set; } diff --git a/rest_service/Entities/Player.cs b/rest_service/Entities/Player.cs index 4b6c33e..d77c31e 100644 --- a/rest_service/Entities/Player.cs +++ b/rest_service/Entities/Player.cs @@ -1,20 +1,15 @@ -using System.Diagnostics.CodeAnalysis; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -using Newtonsoft.Json; -using RestService.Dtos.RequestObjects; namespace RestService.Entities; -[SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] +[BsonIgnoreExtraElements] public class Player { [BsonElement("_id")] public ObjectId? Id { get; set; } [BsonElement("Nickname")] public string? Name { get; set; } [BsonElement("TeamName")] public string? Team { get; set; } [BsonElement("Email")] public string? Email { get; set; } - - [JsonProperty("location")] [BsonElement("location")] public string? Location { get; set; } } \ No newline at end of file diff --git a/rest_service/Entities/PlayerUnique.cs b/rest_service/Entities/PlayerUnique.cs index 221835c..9b6a414 100644 --- a/rest_service/Entities/PlayerUnique.cs +++ b/rest_service/Entities/PlayerUnique.cs @@ -1,18 +1,14 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -using Newtonsoft.Json; +namespace RestService.Entities; -namespace RestService.Entities +[BsonIgnoreExtraElements] +public class PlayerUnique { - public class PlayerUnique - { - [BsonId] - [JsonProperty("Name")] - public string Name { get; set; } + [BsonId] + public string Name { get; set; } - [JsonProperty("location")] - [BsonElement("location")] - public string? Location { get; set; } - } + [BsonElement("location")] + public string? Location { get; set; } } \ No newline at end of file diff --git a/rest_service/Entities/Position.cs b/rest_service/Entities/Position.cs index 58c72cb..7c27d9e 100644 --- a/rest_service/Entities/Position.cs +++ b/rest_service/Entities/Position.cs @@ -1,11 +1,11 @@ -using System.Diagnostics.CodeAnalysis; +using MongoDB.Bson.Serialization.Attributes; namespace RestService.Entities; -[SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] +[BsonIgnoreExtraElements] public class Position { - public float X { get; set; } - public float Y { get; set; } - public float Z { get; set; } + public double X { get; set; } + public double Y { get; set; } + public double Z { get; set; } } \ No newline at end of file diff --git a/rest_service/Entities/Recording.cs b/rest_service/Entities/Recording.cs index 81a5ec0..4dc0fdf 100644 --- a/rest_service/Entities/Recording.cs +++ b/rest_service/Entities/Recording.cs @@ -1,15 +1,21 @@ -using System.Diagnostics.CodeAnalysis; +using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; namespace RestService.Entities; -[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] +[BsonIgnoreExtraElements] public class Recording { - [BsonElement("location")] public string? Location { get; set; } + [BsonElement("_id")] public ObjectId? Id { get; set; } + [BsonElement("location")] + public string? Location { get; set; } public SessionStatisticsPlain? SessionStatisticsPlain { get; set; } public DateTime? DateTime { get; set; } public RecordingPlayer Player { get; set; } public List? Snapshots { get; set; } public RecordingEvent Event { get; set; } + [BsonElement("speed_vector")] + public double[]? SpeedVector { get; set; } + [BsonElement("accel_vector")] + public double[]? AccelVector { get; set; } } \ No newline at end of file diff --git a/rest_service/Entities/RecordingEvent.cs b/rest_service/Entities/RecordingEvent.cs index f079872..198a5aa 100644 --- a/rest_service/Entities/RecordingEvent.cs +++ b/rest_service/Entities/RecordingEvent.cs @@ -1,9 +1,11 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Serializers; namespace RestService.Entities; +[BsonIgnoreExtraElements] public class RecordingEvent : Event { // This class is empty intentionally. @@ -12,11 +14,15 @@ public class RecordingEvent : Event // https://www.mongodb.com/blog/post/building-with-patterns-the-extended-reference-pattern } -public class EventForRecordingSerializer : SerializerBase +public class RecordingEventSerializer : SerializerBase, IBsonDocumentSerializer { public override RecordingEvent Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) { - return BsonSerializer.Deserialize(context.Reader); + var deserialized = BsonDocumentSerializer.Instance.Deserialize(context); + return new RecordingEvent() + { + Id = deserialized.GetValue("_id").ToString() + }; } public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, RecordingEvent value) @@ -28,4 +34,23 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati BsonDocumentSerializer.Instance.Serialize(context, document); } + + public bool TryGetMemberSerializationInfo(string memberName, out BsonSerializationInfo? serializationInfo) + { + switch (memberName) + { + case "Id": + serializationInfo = new BsonSerializationInfo("_id", ObjectIdSerializer.Instance, typeof(ObjectId)); + return true; + case "Name": + serializationInfo = new BsonSerializationInfo("name", StringSerializer.Instance, typeof(string)); + return true; + case "Location": + serializationInfo = new BsonSerializationInfo("location", StringSerializer.Instance, typeof(string)); + return true; + default: + serializationInfo = null; + return false; + } + } } \ No newline at end of file diff --git a/rest_service/Entities/RecordingPlayer.cs b/rest_service/Entities/RecordingPlayer.cs index 3bc0ce9..3da2b5d 100644 --- a/rest_service/Entities/RecordingPlayer.cs +++ b/rest_service/Entities/RecordingPlayer.cs @@ -1,22 +1,29 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Serializers; namespace RestService.Entities; +[BsonIgnoreExtraElements] public class RecordingPlayer : Player { // This class is empty intentionally. // Its purpose is to override the serialization to store a subset of the parent class // Thus implementing the Extended Reference Schema Design Pattern: - // https://www.mongodb.com/blog/post/building-with-patterns-the-extended-reference-pattern + // https://www.mongodb.com/blog/post/building-with-patterns-the-extended-reference-pattern } -public class PlayerForRecordingsSerializer : SerializerBase +public class RecordingPlayerSerializer : SerializerBase, IBsonDocumentSerializer { public override RecordingPlayer Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) { - return BsonSerializer.Deserialize(context.Reader); + var deserialized = BsonDocumentSerializer.Instance.Deserialize(context); + return new RecordingPlayer() + { + Name = deserialized.GetValue("Nickname").ToString(), + Location = deserialized.GetValue("location").ToString() + }; } public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, RecordingPlayer value) @@ -29,4 +36,29 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati BsonDocumentSerializer.Instance.Serialize(context, document); } + + public bool TryGetMemberSerializationInfo(string memberName, out BsonSerializationInfo? serializationInfo) + { + switch (memberName) + { + case "Id": + serializationInfo = new BsonSerializationInfo("_id", new ObjectIdSerializer(), typeof(ObjectId)); + return true; + case "Name": + serializationInfo = new BsonSerializationInfo("Nickname", new StringSerializer(), typeof(string)); + return true; + case "Team": + serializationInfo = new BsonSerializationInfo("TeamName", new StringSerializer(), typeof(string)); + return true; + case "Email": + serializationInfo = new BsonSerializationInfo("Email", new StringSerializer(), typeof(string)); + return true; + case "Location": + serializationInfo = new BsonSerializationInfo("location", new StringSerializer(), typeof(string)); + return true; + default: + serializationInfo = null; + return false; + } + } } \ No newline at end of file diff --git a/rest_service/Entities/SessionStatisticsPlain.cs b/rest_service/Entities/SessionStatisticsPlain.cs index 3057399..edb8bee 100644 --- a/rest_service/Entities/SessionStatisticsPlain.cs +++ b/rest_service/Entities/SessionStatisticsPlain.cs @@ -1,8 +1,8 @@ -using System.Diagnostics.CodeAnalysis; +using MongoDB.Bson.Serialization.Attributes; namespace RestService.Entities; -[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] +[BsonIgnoreExtraElements] public class SessionStatisticsPlain { public int BulletsFired { get; set; } diff --git a/rest_service/Entities/Snapshot.cs b/rest_service/Entities/Snapshot.cs index 3d5a955..dc8e66d 100644 --- a/rest_service/Entities/Snapshot.cs +++ b/rest_service/Entities/Snapshot.cs @@ -1,10 +1,8 @@ -using System.Diagnostics.CodeAnalysis; - -#pragma warning disable CS8618 +using MongoDB.Bson.Serialization.Attributes; namespace RestService.Entities; -[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] +[BsonIgnoreExtraElements] public class Snapshot { public Position Position { get; set; } diff --git a/rest_service/Program.cs b/rest_service/Program.cs index b766a73..9812b5e 100644 --- a/rest_service/Program.cs +++ b/rest_service/Program.cs @@ -8,7 +8,10 @@ builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); + +// required for methods in RecordingsController with diff name but same signature builder.Services.AddSwaggerGen(); + builder.Services.AddCors(options => { options.AddPolicy("NewPolicy", builder => @@ -33,7 +36,6 @@ app.MapControllers(); // Register custom serializers -BsonSerializer.RegisterSerializer(new PlayerForRecordingsSerializer()); -BsonSerializer.RegisterSerializer(new EventForRecordingSerializer()); - +BsonSerializer.RegisterSerializer(new RecordingPlayerSerializer()); +BsonSerializer.RegisterSerializer(new RecordingEventSerializer()); app.Run(); \ No newline at end of file diff --git a/website/.env.template b/website/.env.template index 107eb16..2d32f61 100644 --- a/website/.env.template +++ b/website/.env.template @@ -4,4 +4,5 @@ ATLAS_CHART_EMBED_DASHBOARD_URL= ATLAS_CHART_ID_EVENT= ATLAS_CHART_ID_PLAYER= ATLAS_CHART_ID_HOME= +ATLAS_CHART_ID_SIMILAR= GAME_CLIENT_PORT=8000 diff --git a/website/Data/SimilarRecordings.cs b/website/Data/SimilarRecordings.cs new file mode 100644 index 0000000..2d83a85 --- /dev/null +++ b/website/Data/SimilarRecordings.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +namespace website.Data; + +public class SessionStatisticsPlain +{ + public int BulletsFired { get; set; } + public int DamageDone { get; set; } + public int PelletsDestroyedSmall { get; set; } + public int PelletsDestroyedMedium { get; set; } + public int PelletsDestroyedLarge { get; set; } + public int Score { get; set; } + public int PowerUpBulletDamageCollected { get; set; } + public int PowerUpBulletSpeedCollected { get; set; } + public int PowerUpPlayerSpeedCollected { get; set; } +} + +public class SimilarRecording +{ + [JsonProperty("id")] + public string? Id { get; set; } + [JsonProperty("sessionStatisticsPlain")] + public SessionStatisticsPlain? SessionStatisticsPlain { get; set; } + [JsonProperty("name")] + public string? Name { get; set; } +} \ No newline at end of file diff --git a/website/Pages/PlayerHome.razor b/website/Pages/PlayerHome.razor index 4fd4afb..767df7f 100644 --- a/website/Pages/PlayerHome.razor +++ b/website/Pages/PlayerHome.razor @@ -115,6 +115,13 @@ } else { +
+
+     + View Similar Players to @Player.Name +     +
+
} @@ -139,18 +146,18 @@ _eventId = queryParameters[Constants.QueryParameterEventId]; var eventsFilter = new Dictionary -{ -{ "id", _eventId } -}; + { + { "id", _eventId } + }; string eventsUrlWithQuery = UrlHelper.BuildUrlWithQuery(Constants.RestServiceEndpointEvents, eventsFilter); var events = await _restClient.GetJsonAsync>(eventsUrlWithQuery); Event = events.First(); _name = queryParameters[Constants.QueryParameterName]; var playerFilter = new Dictionary -{ -{ "name", _name } -}; + { + { "name", _name } + }; string playersUrlWithQuery = UrlHelper.BuildUrlWithQuery(Constants.RestServiceEndpointPlayers, playerFilter); var players = await _restClient.GetJsonAsync>(playersUrlWithQuery); Player = players.First(); diff --git a/website/Pages/PlayerSimilar.razor b/website/Pages/PlayerSimilar.razor new file mode 100644 index 0000000..3ec4c2e --- /dev/null +++ b/website/Pages/PlayerSimilar.razor @@ -0,0 +1,164 @@ +@page "/playerSimilar" +@using RestSharp +@using website.Data +@using website.Utils +@using dotenv.net + +@inject NavigationManager NavigationManager +@inject Blazored.LocalStorage.ILocalStorageService localStore +@inject IJSRuntime JSRuntime + + + +logo + +
+ @if (Player == null) + { +

+ Loading... +

+ } + else + { +
+

Player Dashboard

+

+ Similar Players by Speed based on Highest Score Run +

+

+ @_errorMessage +

+ +
+
+
+ + + +
+ +
+ +
+
+ + + +
+ +
+ +
+
+ + + +
+ + +
+
+ +
+ } + +
+ + +
+ @if (Player == null) + { +

+ Loading... +

+ } + else + { +

Similar Players by Speed

+ foreach (SimilarRecording rec in SimilarBySpeed) + { +
@rec.Name
+ string chartUrl = ChartsUrl.CreateSimilarUrl(_atlasChartIdSimilar, rec.Id); + + } + + //TO-DO: Revisit acceleration + + } +
+ + +@code { + + private Player Player { get; set; } = new(); + private List SimilarBySpeed { get; set; } = new(); + private List SimilarByAccel { get; set; } = new(); + + private string _name = string.Empty; + private string _atlasChartIdSimilar = string.Empty; + private string _chartUrl = string.Empty; + private string _errorMessage = string.Empty; + private readonly RestClient _restClient = RestServiceClient.Create(); + + private string chartFilter = string.Empty; + + protected override async Task OnInitializedAsync() + { + try + { + var queryParameters = UrlHelper.GetParameters(NavigationManager.Uri); + + _name = queryParameters[Constants.QueryParameterName]; + var playerFilter = new Dictionary + { + { "name", _name } + }; + + string playersUrlWithQuery = UrlHelper.BuildUrlWithQuery(Constants.RestServiceEndpointPlayers, playerFilter); + var players = await _restClient.GetJsonAsync>(playersUrlWithQuery); + Player = players.First(); + + string similarBySpeedUrlWithQuery = UrlHelper.BuildUrlWithQuery(Constants.RestServiceEndpointSimilarBySpeed, playerFilter); + SimilarBySpeed = await _restClient.GetJsonAsync>(similarBySpeedUrlWithQuery); + + //TO-DO: Revisit acceleration + //string similarByAccelUrlWithQuery = UrlHelper.BuildUrlWithQuery(Constants.RestServiceEndpointSimilarByAccel, playerFilter); + //SimilarByAccel = await _restClient.GetJsonAsync>(similarByAccelUrlWithQuery); + + DotEnv.Load(); + var envVars = DotEnv.Read(); + _atlasChartIdSimilar = envVars["ATLAS_CHART_ID_SIMILAR"]; + } + catch (Exception e) + { + Console.WriteLine(e.Message); + Console.WriteLine(e.StackTrace); + _errorMessage = "PLAYER NOT FOUND"; + } + } +} \ No newline at end of file diff --git a/website/Utils/ChartsUrl.cs b/website/Utils/ChartsUrl.cs index fa46e8b..95827da 100644 --- a/website/Utils/ChartsUrl.cs +++ b/website/Utils/ChartsUrl.cs @@ -26,6 +26,14 @@ public static string CreatePlayerUrl(string chartsId, string name, string locati + "%27}"; } + public static string CreateSimilarUrl(string chartsId, string recordingId) + { + return CreateBaseUrl(chartsId) + + "&filter={%27_id%27:{%27$oid%27:%27" + + recordingId + + "%27}}"; + } + public static string CreateHomeUrl(string chartsId, string eventId, string eventLocation) { return CreateBaseUrl(chartsId) diff --git a/website/Utils/Constants.cs b/website/Utils/Constants.cs index 4683e16..75256fa 100644 --- a/website/Utils/Constants.cs +++ b/website/Utils/Constants.cs @@ -8,6 +8,8 @@ public static class Constants public const string RestServiceEndpointPlayers = "players"; public const string RestServiceEndpointPlayersAutoComplete = "players/autocomplete"; public const string RestServiceEndpointPlayersSearch = "players/search"; + public const string RestServiceEndpointSimilarBySpeed = "recordings/similarBySpeed"; + public const string RestServiceEndpointSimilarByAccel = "recordings/similarByAcceleration"; public const string QueryParameterEventId = "EventId"; public const string QueryParameterName = "Name"; } \ No newline at end of file