Skip to content

Migrate scores to use proper relationships #830

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Aug 19, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ public IActionResult GetResource(string hash)
string fullPath = Path.GetFullPath(path);

// Prevent directory traversal attacks
if (!fullPath.StartsWith(FileHelper.FullResourcePath)) return this.BadRequest();
if (!fullPath.StartsWith(FileHelper.FullResourcePath)) return this.StatusCode(400);

if (FileHelper.ResourceExists(hash)) return this.File(IOFile.OpenRead(path), "application/octet-stream");

return this.NotFound();
return this.StatusCode(404);
}

// TODO: check if this is a valid hash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,15 @@ public ScoreController(DatabaseContext database)
this.database = database;
}

private string[] getFriendUsernames(int userId, string username)
private static int[] GetFriendIds(int userId)
{
UserFriendData? store = UserFriendStore.GetUserFriendData(userId);
if (store == null) return new[] { username, };

List<string> friendNames = new()
List<int>? friendIds = store?.FriendIds;
friendIds ??= new List<int>
{
username,
userId,
};

// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
foreach (int friendId in store.FriendIds)
{
string? friendUsername = this.database.Users.Where(u => u.UserId == friendId)
.Select(u => u.Username)
.FirstOrDefault();
if (friendUsername != null) friendNames.Add(friendUsername);
}

return friendNames.ToArray();
return friendIds.Append(userId).Distinct().ToArray();
}

[HttpPost("scoreboard/{slotType}/{id:int}")]
Expand Down Expand Up @@ -144,41 +133,41 @@ public async Task<IActionResult> SubmitScore(string slotType, int id, int childI

await this.database.SaveChangesAsync();

string playerIdCollection = string.Join(',', score.PlayerIds);

ScoreEntity? existingScore = await this.database.Scores.Where(s => s.SlotId == slot.SlotId)
.Where(s => s.ChildSlotId == 0 || s.ChildSlotId == childId)
.Where(s => s.PlayerIdCollection == playerIdCollection)
.Where(s => s.UserId == token.UserId)
.Where(s => s.Type == score.Type)
.FirstOrDefaultAsync();
if (existingScore != null)
{
existingScore.Points = Math.Max(existingScore.Points, score.Points);
existingScore.Timestamp = TimeHelper.TimestampMillis;
}
else
{
ScoreEntity playerScore = new()
{
PlayerIdCollection = playerIdCollection,
UserId = token.UserId,
Type = score.Type,
Points = score.Points,
SlotId = slotId,
ChildSlotId = childId,
Timestamp = TimeHelper.TimestampMillis,
};
this.database.Scores.Add(playerScore);
}

await this.database.SaveChangesAsync();

return this.Ok(this.getScores(new LeaderboardOptions
return this.Ok(await this.GetScores(new LeaderboardOptions
{
RootName = "scoreboardSegment",
PageSize = 5,
PageStart = -1,
SlotId = slotId,
ChildSlotId = childId,
ScoreType = score.Type,
TargetUsername = username,
TargetUser = token.UserId,
TargetPlayerIds = null,
}));
}
Expand All @@ -189,8 +178,6 @@ public async Task<IActionResult> Lbp1Leaderboards(string slotType, int id)
{
GameTokenEntity token = this.GetToken();

string username = await this.database.UsernameFromGameToken(token);

if (slotType == "developer") id = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer);

LeaderboardOptions options = new()
Expand All @@ -199,7 +186,7 @@ public async Task<IActionResult> Lbp1Leaderboards(string slotType, int id)
PageStart = 1,
ScoreType = -1,
SlotId = id,
TargetUsername = username,
TargetUser = token.UserId,
RootName = "scoreboardSegment",
};
if (!HttpMethods.IsPost(this.Request.Method))
Expand All @@ -208,7 +195,7 @@ public async Task<IActionResult> Lbp1Leaderboards(string slotType, int id)
for (int i = 1; i <= 4; i++)
{
options.ScoreType = i;
ScoreboardResponse response = this.getScores(options);
ScoreboardResponse response = await this.GetScores(options);
scoreboardResponses.Add(new PlayerScoreboardResponse(response.Scores, i));
}
return this.Ok(new MultiScoreboardResponse(scoreboardResponses));
Expand All @@ -217,9 +204,9 @@ public async Task<IActionResult> Lbp1Leaderboards(string slotType, int id)
GameScore? score = await this.DeserializeBody<GameScore>();
if (score == null) return this.BadRequest();
options.ScoreType = score.Type;
options.TargetPlayerIds = this.getFriendUsernames(token.UserId, username);
options.TargetPlayerIds = GetFriendIds(token.UserId);

return this.Ok(this.getScores(options));
return this.Ok(await this.GetScores(options));
}

[HttpGet("friendscores/{slotType}/{slotId:int}/{type:int}")]
Expand All @@ -230,96 +217,94 @@ public async Task<IActionResult> FriendScores(string slotType, int slotId, int?

if (pageSize <= 0) return this.BadRequest();

string username = await this.database.UsernameFromGameToken(token);

if (SlotHelper.IsTypeInvalid(slotType)) return this.BadRequest();

if (slotType == "developer") slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer);

string[] friendIds = this.getFriendUsernames(token.UserId, username);
int[] friendIds = GetFriendIds(token.UserId);

return this.Ok(this.getScores(new LeaderboardOptions
return this.Ok(await this.GetScores(new LeaderboardOptions
{
RootName = "scores",
PageSize = pageSize,
PageStart = pageStart,
SlotId = slotId,
ChildSlotId = childId,
ScoreType = type,
TargetUsername = username,
TargetUser = token.UserId,
TargetPlayerIds = friendIds,
}));
}

[HttpGet("topscores/{slotType}/{slotId:int}/{type:int}")]
[HttpGet("topscores/{slotType}/{slotId:int}/{childId:int}/{type:int}")]
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
public async Task<IActionResult> TopScores(string slotType, int slotId, int? childId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5)
{
GameTokenEntity token = this.GetToken();

if (pageSize <= 0) return this.BadRequest();

string username = await this.database.UsernameFromGameToken(token);

if (SlotHelper.IsTypeInvalid(slotType)) return this.BadRequest();

if (slotType == "developer") slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer);

return this.Ok(this.getScores(new LeaderboardOptions
return this.Ok(await this.GetScores(new LeaderboardOptions
{
RootName = "scores",
PageSize = pageSize,
PageStart = pageStart,
SlotId = slotId,
ChildSlotId = childId,
ScoreType = type,
TargetUsername = username,
TargetUser = token.UserId,
TargetPlayerIds = null,
}));
}

private class LeaderboardOptions
{
public int SlotId { get; set; }
public int SlotId { get; init; }
public int ScoreType { get; set; }
public string TargetUsername { get; set; } = "";
public int PageStart { get; set; } = -1;
public int PageSize { get; set; } = 5;
public string RootName { get; set; } = "scores";
public string[]? TargetPlayerIds;
public int TargetUser { get; init; }
public int PageStart { get; init; } = -1;
public int PageSize { get; init; } = 5;
public string RootName { get; init; } = "scores";
public int[]? TargetPlayerIds;
public int? ChildSlotId;
}

private ScoreboardResponse getScores(LeaderboardOptions options)
private async Task<ScoreboardResponse> GetScores(LeaderboardOptions options)
{
// This is hella ugly but it technically assigns the proper rank to a score
// var needed for Anonymous type returned from SELECT
var rankedScores = this.database.Scores.Where(s => s.SlotId == options.SlotId)
IQueryable<ScoreEntity> scoreQuery = this.database.Scores.Where(s => s.SlotId == options.SlotId)
.Where(s => options.ScoreType == -1 || s.Type == options.ScoreType)
.Where(s => s.ChildSlotId == 0 || s.ChildSlotId == options.ChildSlotId)
.AsEnumerable()
.Where(s => options.TargetPlayerIds == null ||
options.TargetPlayerIds.Any(id => s.PlayerIdCollection.Split(",").Contains(id)))
.OrderByDescending(s => s.Points)
.ThenBy(s => s.ScoreId)
.ToList()
.Select((s, rank) => new
.Where(s => options.TargetPlayerIds == null || options.TargetPlayerIds.Contains(s.UserId));

// First find if you have a score on a level to find scores around it
var myScore = await scoreQuery.Where(s => s.UserId == options.TargetUser)
.Select(s => new
{
Score = s,
Rank = rank + 1,
})
.ToList();
Rank = scoreQuery.Count(s2 => s2.Points > s.Points)+1,
}).FirstOrDefaultAsync();

int skipAmt = options.PageStart != -1 || myScore == null ? options.PageStart - 1 : myScore.Rank - 3;

// Find your score, since even if you aren't in the top list your score is pinned
var myScore = rankedScores.Where(rs => rs.Score.PlayerIdCollection.Split(",").Contains(options.TargetUsername)).MaxBy(rs => rs.Score.Points);
var rankedScores = scoreQuery.OrderByDescending(s => s.Points)
.ThenBy(s => s.ScoreId)
.Skip(Math.Max(0, skipAmt))
.Take(Math.Min(options.PageSize, 30))
.Select(s => new
{
Score = s,
Rank = scoreQuery.Count(s2 => s2.Points > s.Points)+1,
})
.ToList();

// Paginated viewing: if not requesting pageStart, get results around user
var pagedScores = rankedScores.Skip(options.PageStart != -1 || myScore == null ? options.PageStart - 1 : myScore.Rank - 3).Take(Math.Min(options.PageSize, 30));
int totalScores = scoreQuery.Count();

List<GameScore> gameScores = pagedScores.ToSerializableList(ps => GameScore.CreateFromEntity(ps.Score, ps.Rank));
List<GameScore> gameScores = rankedScores.ToSerializableList(ps => GameScore.CreateFromEntity(ps.Score, ps.Rank));

return new ScoreboardResponse(options.RootName, gameScores, rankedScores.Count, myScore?.Score.Points ?? 0, myScore?.Rank ?? 0);
return new ScoreboardResponse(options.RootName, gameScores, totalScores, myScore?.Score.Points ?? 0, myScore?.Rank ?? 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
@for(int i = 0; i < Model.Scores.Count; i++)
{
ScoreEntity score = Model.Scores[i];
string[] playerIds = score.PlayerIds;
DatabaseContext database = Model.Database;
<div class="item">
<span class="ui large text">
Expand All @@ -39,23 +38,18 @@
</span>
<div class="content">
<div class="list" style="padding-top: 0">
@for (int j = 0; j < playerIds.Length; j++)
{
UserEntity? user = await database.Users.FirstOrDefaultAsync(u => u.Username == playerIds[j]);
@{
UserEntity? user = await database.Users.FindAsync(score.UserId);
}
<div class="item">
<i class="minus icon" style="padding-top: 9px"></i>
<div class="content" style="padding-left: 0">
@if (user != null)
{
@await user.ToLink(Html, ViewData, language, timeZone)
}
else
{
<p style="margin-top: 5px;">@playerIds[j]</p>
}
</div>
</div>
}
</div>
</div>
</div>
Expand Down
34 changes: 19 additions & 15 deletions ProjectLighthouse/Administration/Maintenance/MaintenanceHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ public static class MaintenanceHelper
{
static MaintenanceHelper()
{
Commands = getListOfInterfaceObjects<ICommand>();
MaintenanceJobs = getListOfInterfaceObjects<IMaintenanceJob>();
MigrationTasks = getListOfInterfaceObjects<IMigrationTask>();
RepeatingTasks = getListOfInterfaceObjects<IRepeatingTask>();
Commands = GetListOfInterfaceObjects<ICommand>();
MaintenanceJobs = GetListOfInterfaceObjects<IMaintenanceJob>();
MigrationTasks = GetListOfInterfaceObjects<MigrationTask>();
RepeatingTasks = GetListOfInterfaceObjects<IRepeatingTask>();
}

public static List<ICommand> Commands { get; }
public static List<IMaintenanceJob> MaintenanceJobs { get; }
public static List<IMigrationTask> MigrationTasks { get; }
public static List<MigrationTask> MigrationTasks { get; }
public static List<IRepeatingTask> RepeatingTasks { get; }

public static async Task<List<LogLine>> RunCommand(IServiceProvider provider, string[] args)
Expand Down Expand Up @@ -80,16 +80,18 @@ public static async Task RunMaintenanceJob(string jobName)
await job.Run();
}

public static async Task RunMigration(DatabaseContext database, IMigrationTask migrationTask)
public static async Task<bool> RunMigration(DatabaseContext database, MigrationTask migrationTask)
{

// Migrations should never be run twice.
Debug.Assert(!await database.CompletedMigrations.Has(m => m.MigrationName == migrationTask.GetType().Name));
Debug.Assert(!await database.CompletedMigrations.Has(m => m.MigrationName == migrationTask.GetType().Name),
$"Tried to run migration {migrationTask.GetType().Name} twice");

Logger.Info($"Running migration task {migrationTask.Name()}", LogArea.Database);
Logger.Info($"Running LH migration task {migrationTask.Name()}", LogArea.Database);

bool success;
Exception? exception = null;

Stopwatch stopwatch = Stopwatch.StartNew();

try
{
Expand All @@ -103,13 +105,14 @@ public static async Task RunMigration(DatabaseContext database, IMigrationTask m

if (!success)
{
Logger.Error($"Could not run migration {migrationTask.Name()}", LogArea.Database);
Logger.Error($"Could not run LH migration {migrationTask.Name()}", LogArea.Database);
if (exception != null) Logger.Error(exception.ToDetailedException(), LogArea.Database);

return;
return false;
}

Logger.Success($"Successfully completed migration {migrationTask.Name()}", LogArea.Database);
stopwatch.Stop();

Logger.Success($"Successfully completed LH migration {migrationTask.Name()} in {stopwatch.ElapsedMilliseconds}", LogArea.Database);

CompletedMigrationEntity completedMigration = new()
{
Expand All @@ -119,13 +122,14 @@ public static async Task RunMigration(DatabaseContext database, IMigrationTask m

database.CompletedMigrations.Add(completedMigration);
await database.SaveChangesAsync();
return true;
}

private static List<T> getListOfInterfaceObjects<T>() where T : class
private static List<T> GetListOfInterfaceObjects<T>() where T : class
{
return Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.GetInterfaces().Contains(typeof(T)) && t.GetConstructor(Type.EmptyTypes) != null)
.Where(t => (t.IsSubclassOf(typeof(T)) || t.GetInterfaces().Contains(typeof(T))) && t.GetConstructor(Type.EmptyTypes) != null)
.Select(t => Activator.CreateInstance(t) as T)
.ToList()!;
}
Expand Down
Loading