Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
121 changes: 114 additions & 7 deletions Refresh.Database/GameDatabaseContext.Pins.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Refresh.Database.Models.Authentication;
using Refresh.Database.Models.Users;
using Refresh.Database.Models.Relations;
using Refresh.Database.Models.Pins;

namespace Refresh.Database;

Expand All @@ -11,30 +12,43 @@ public void UpdateUserPinProgress(Dictionary<long, int> pinProgressUpdates, Game
DateTimeOffset now = this._time.Now;
bool isBeta = game == TokenGame.BetaBuild;
IEnumerable<PinProgressRelation> existingProgresses = this.GetPinProgressesByUser(user, isBeta);

List<long> descendingProgressPins =
[
(long)ServerPins.TopXOfAnyStoryLevelWithOver50Scores,
(long)ServerPins.TopXOfAnyCommunityLevelWithOver50Scores,
];

this.Write(() =>
{
foreach (KeyValuePair<long, int> pinProgressUpdate in pinProgressUpdates)
{
PinProgressRelation? existingProgress = existingProgresses.FirstOrDefault(p => p.PinId == pinProgressUpdate.Key);
long pinId = pinProgressUpdate.Key;
int newProgress = pinProgressUpdate.Value;
PinProgressRelation? existingProgress = existingProgresses.FirstOrDefault(p => p.PinId == pinId);

if (existingProgress == null)
{
PinProgressRelation newRelation = new()
{
PinId = pinProgressUpdate.Key,
Progress = pinProgressUpdate.Value,
PinId = pinId,
Progress = newProgress,
Publisher = user,
FirstPublished = now,
LastUpdated = now,
IsBeta = isBeta,
};
this.PinProgressRelations.Add(newRelation);
continue;
}
// Only update if the new progress is actually better
else if (pinProgressUpdate.Value > existingProgress.Progress)

bool isSpecialTreatmentPin = descendingProgressPins.Contains(pinId);

// Only update progress if it's better. For most pins it's better the greater it is, but for the pins in
// specialTreatmentPins, it's better the smaller it is.
if (!isSpecialTreatmentPin && newProgress > existingProgress.Progress
|| isSpecialTreatmentPin && newProgress < existingProgress.Progress)
{
existingProgress.Progress = pinProgressUpdate.Value;
existingProgress.Progress = newProgress;
existingProgress.LastUpdated = now;
}
}
Expand Down Expand Up @@ -83,6 +97,96 @@ public void UpdateUserProfilePins(List<long> pinUpdates, GameUser user, TokenGam
});
}

public PinProgressRelation UpdateUserPinProgressToLowest(long pinId, int newProgressValue, GameUser user, bool isBeta)
{
// Get pin progress if it exists already
PinProgressRelation? progressToUpdate = this.PinProgressRelations.FirstOrDefault(p => p.PinId == pinId && p.PublisherId == user.UserId && p.IsBeta == isBeta);
DateTimeOffset now = this._time.Now;

if (progressToUpdate == null)
{
this.Write(() =>
{
progressToUpdate = new()
{
PinId = pinId,
Progress = newProgressValue,
Publisher = user,
PublisherId = user.UserId,
FirstPublished = now,
LastUpdated = now,
IsBeta = isBeta,
};

this.PinProgressRelations.Add(progressToUpdate);
});
}
// Only update if the final progress value is actually lower to the one already set
else if (newProgressValue < progressToUpdate.Progress)
{
this.Write(() =>
{
progressToUpdate.Progress = newProgressValue;
progressToUpdate.LastUpdated = now;
});
}

return progressToUpdate!;
}

public void IncrementUserPinProgress(long pinId, int progressToAdd, GameUser user)
{
this.Write(() =>
{
this.IncrementUserPinProgressInternal(pinId, progressToAdd, user, true);
this.IncrementUserPinProgressInternal(pinId, progressToAdd, user, false);
});

}

public PinProgressRelation IncrementUserPinProgress(long pinId, int progressToAdd, GameUser user, bool isBeta)
{
PinProgressRelation relation = null!;

this.Write(() =>
{
relation = this.IncrementUserPinProgressInternal(pinId, progressToAdd, user, isBeta);
});

return relation;
}

private PinProgressRelation IncrementUserPinProgressInternal(long pinId, int progressToAdd, GameUser user, bool isBeta)
{
// Get pin progress if it exists already
PinProgressRelation? progressToUpdate = this.PinProgressRelations.FirstOrDefault(p => p.PinId == pinId && p.PublisherId == user.UserId && p.IsBeta == isBeta);
DateTimeOffset now = this._time.Now;

if (progressToUpdate == null)
{
PinProgressRelation newRelation = new()
{
PinId = pinId,
Progress = progressToAdd,
Publisher = user,
PublisherId = user.UserId,
FirstPublished = now,
LastUpdated = now,
IsBeta = isBeta,
};

this.PinProgressRelations.Add(newRelation);
return newRelation;
}
else
{
progressToUpdate.Progress =+ progressToAdd;
progressToUpdate.LastUpdated = now;
}

return progressToUpdate;
}

private IEnumerable<PinProgressRelation> GetPinProgressesByUser(GameUser user, bool isBeta)
=> this.PinProgressRelations
.Where(p => p.Publisher == user && p.IsBeta == isBeta)
Expand All @@ -91,6 +195,9 @@ private IEnumerable<PinProgressRelation> GetPinProgressesByUser(GameUser user, b
public DatabaseList<PinProgressRelation> GetPinProgressesByUser(GameUser user, TokenGame game, int skip, int count)
=> new(this.GetPinProgressesByUser(user, game == TokenGame.BetaBuild), skip, count);

public PinProgressRelation? GetUserPinProgress(long pinId, GameUser user, bool isBeta)
=> this.PinProgressRelations.FirstOrDefault(p => p.PinId == pinId && p.PublisherId == user.UserId && p.IsBeta == isBeta);

private IEnumerable<ProfilePinRelation> GetProfilePinsByUser(GameUser user, TokenGame game)
=> this.ProfilePinRelations
.Where(p => p.Publisher == user && p.Game == game)
Expand Down
18 changes: 18 additions & 0 deletions Refresh.Database/Models/Pins/ManuallyAwardedPins.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Refresh.Database.Models.Pins;

/// <summary>
/// The progress types of pins which have to be awarded manually by the server.
/// </summary>
public enum ServerPins : long
{
// Level Leaderboards
TopFourthOfXStoryLevelsWithOver50Scores = 3394094772,
TopFourthOfXCommunityLevelsWithOver50Scores = 1700253570,
TopXOfAnyStoryLevelWithOver50Scores = 191183438,
TopXOfAnyCommunityLevelWithOver50Scores = 2033315234,

// Website
SignIntoWebsite = 2691148325,
HeartPlayerOnWebsite = 1965011384,
QueueLevelOnWebsite = 2833810997,
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Refresh.Core.Types.Data;
using Refresh.Database;
using Refresh.Database.Models.Authentication;
using Refresh.Database.Models.Pins;
using Refresh.Database.Models.Relations;
using Refresh.Database.Models.Users;
using Refresh.Interfaces.APIv3.Endpoints.ApiTypes;
Expand Down Expand Up @@ -107,6 +108,9 @@ public ApiResponse<IApiAuthenticationResponse> Authenticate(RequestContext conte

context.Logger.LogInfo(BunkumCategory.Authentication, $"{user} successfully logged in through the API");

// Update pin progress for signing into the API
database.IncrementUserPinProgress((long)ServerPins.SignIntoWebsite, 1, user);

return new ApiAuthenticationResponse
{
RefreshTokenData = refreshToken.TokenData,
Expand Down
5 changes: 5 additions & 0 deletions Refresh.Interfaces.APIv3/Endpoints/LevelApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Refresh.Database;
using Refresh.Database.Models.Authentication;
using Refresh.Database.Models.Levels;
using Refresh.Database.Models.Pins;
using Refresh.Database.Models.Users;
using Refresh.Database.Query;
using Refresh.Interfaces.APIv3.Documentation.Attributes;
Expand Down Expand Up @@ -231,6 +232,10 @@ public ApiOkResponse QueueLevel(RequestContext context, GameDatabaseContext data
if (level == null) return ApiNotFoundError.LevelMissingError;

database.QueueLevel(level, user);

// Update pin progress for queueing a level through the API
database.IncrementUserPinProgress((long)ServerPins.QueueLevelOnWebsite, 1, user);

return new ApiOkResponse();
}

Expand Down
57 changes: 56 additions & 1 deletion Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Refresh.Database.Models.Authentication;
using Refresh.Database.Models.Levels;
using Refresh.Database.Models.Levels.Scores;
using Refresh.Database.Models.Pins;
using Refresh.Database.Models.Users;
using Refresh.Interfaces.Game.Types.Lists;
using Refresh.Interfaces.Game.Types.Scores;
Expand Down Expand Up @@ -129,10 +130,64 @@ public Response SubmitScore(RequestContext context, GameUser user, GameServerCon

DatabaseList<ScoreWithRank>? scores = database.GetRankedScoresAroundScore(score, 5);
Debug.Assert(scores != null);


this.AwardScoreboardPins(scores, dataContext, user, level);

return new Response(SerializedScoreLeaderboardList.FromDatabaseList(scores, dataContext), ContentType.Xml);
}

/// <summary>
/// Awards certain score submission-related pins which the game expects the server to award
/// </summary>
private void AwardScoreboardPins(DatabaseList<ScoreWithRank> scores, DataContext dataContext, GameUser user, GameLevel level)
{
dataContext.Database.EnsureLevelStatisticsCreated(level);
int uniqueScoreCount = scores.TotalItems;

// All pins below are only expected to be awarded if the level's leaderboard has atleast 50 scores.
// This check also prevents dividing by 0 below.
if (uniqueScoreCount < 50) return;

ScoreWithRank? ownScore = scores.Items.FirstOrDefault(s => s.score.PlayerIds.Contains(user.UserId));
if (ownScore == null) return; // Should never happen, incase it somehow does, skip this part

// Examples for rankingInPercent:
// - rank 20 out of 40 = 50%
// - rank 5 out of 40 = 12.5%
// Always rounding up will prevent users from being top 0% of a leaderboard (since 1% should be the maximum)
// after the int cast; and for the top 25% pins, being in the top 25.001% for example technically
// doesn't count as completing the pins' objective, since that's still greater than 25%.
int rankingInPercent = (int)Math.Ceiling((float)ownScore.rank / uniqueScoreCount * 100);
bool isStoryLevel = level.SlotType == GameSlotType.Story;
bool isGameBetaBuild = dataContext.Game == TokenGame.BetaBuild;

// Update lowest rankingInPercent of any story/user level leaderboard
if (isStoryLevel)
{
dataContext.Database.UpdateUserPinProgressToLowest((long)ServerPins.TopXOfAnyStoryLevelWithOver50Scores,
rankingInPercent, user, isGameBetaBuild);
}
else
{
dataContext.Database.UpdateUserPinProgressToLowest((long)ServerPins.TopXOfAnyCommunityLevelWithOver50Scores,
rankingInPercent, user, isGameBetaBuild);
}

// Update on how many story/user levels the user's ranking is in the top 25% (top 1/4th) of the leaderboard or below
if (rankingInPercent > 25) return;

if (isStoryLevel)
{
dataContext.Database.IncrementUserPinProgress((long)ServerPins.TopFourthOfXStoryLevelsWithOver50Scores,
1, user, isGameBetaBuild);
}
else
{
dataContext.Database.IncrementUserPinProgress((long)ServerPins.TopFourthOfXCommunityLevelsWithOver50Scores,
1, user, isGameBetaBuild);
}
}

private (byte, DateTimeOffset?) GetScoreTypeAndMinAge(int originalType, DateTimeOffset now)
{
return originalType switch
Expand Down
Loading