Skip to content
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

v3.28.2 #598

Merged
merged 8 commits into from
Jan 22, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,18 @@ public void GameImportExportExternalHosts_ExportsProperties()
.Length.ShouldBe(0);
}

// [Fact]
// public void GameImportExportExternalHosts_AttributeStrategy_ExportsProperties()
// {
// var properties = typeof(GameImportExportExternalHost).GetProperties(BindingFlags.Public | BindingFlags.Instance);
// var propertyNames = properties.Select(p => p.Name).ToArray();
// var gameProperties = typeof(ExternalGameHost)
// .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
// .Where(p => !p.HasAttribute<NotMappedAttribute>())
// .Where(p => !p.HasAttribute<DontExportAttribute>());
[Fact]
public void GameImportExportExternalHosts_AttributeStrategy_ExportsProperties()
{
var properties = typeof(GameImportExportExternalHost).GetProperties(BindingFlags.Public | BindingFlags.Instance);
var propertyNames = properties.Select(p => p.Name).ToArray();
var gameProperties = typeof(ExternalGameHost)
.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
.Where(p => !p.HasAttribute<NotMappedAttribute>())
.Where(p => !p.HasAttribute<DontExportAttribute>());

// gameProperties.Where(p => !propertyNames.Contains(p.Name))
// .ToArray()
// .Length.ShouldBe(0);
// }
gameProperties.Where(p => !propertyNames.Contains(p.Name))
.ToArray()
.Length.ShouldBe(0);
}
}
3 changes: 3 additions & 0 deletions src/Gameboard.Api/Data/Entities/ExternalGameHost.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Gameboard.Api.Features.Games;

namespace Gameboard.Api.Data;

Expand All @@ -10,12 +11,14 @@ public sealed class ExternalGameHost : IEntity
public required bool DestroyResourcesOnDeployFailure { get; set; }
public int? GamespaceDeployBatchSize { get; set; }
public int? HttpTimeoutInSeconds { get; set; }
[DontExport]
public string HostApiKey { get; set; }
public required string HostUrl { get; set; }
public string PingEndpoint { get; set; }
public required string StartupEndpoint { get; set; }
public string TeamExtendedEndpoint { get; set; }

// nav properties
[DontExport]
public ICollection<Data.Game> UsedByGames { get; set; } = new List<Data.Game>();
}
13 changes: 0 additions & 13 deletions src/Gameboard.Api/Data/Entities/Game.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,26 +78,13 @@ public class Game : IEntity
public ICollection<Feedback> Feedback { get; set; } = [];
public ICollection<ChallengeGate> Prerequisites { get; set; } = [];

[NotMapped] public bool RequireSession => SessionMinutes > 0;
[NotMapped] public bool RequireTeam => MinTeamSize > 1;
[NotMapped] public bool AllowTeam => MaxTeamSize > 1;

[NotMapped]
public bool IsLive =>
GameStart != DateTimeOffset.MinValue &&
GameStart.CompareTo(DateTimeOffset.UtcNow) < 0 &&
GameEnd.CompareTo(DateTimeOffset.UtcNow) > 0;

[NotMapped]
public bool HasEnded =>
GameEnd.CompareTo(DateTimeOffset.UtcNow) < 0;

[NotMapped]
public bool RegistrationActive =>
RegistrationType != GameRegistrationType.None &&
RegistrationOpen.CompareTo(DateTimeOffset.UtcNow) < 0 &&
RegistrationClose.CompareTo(DateTimeOffset.UtcNow) > 0;

[NotMapped] public bool IsCompetitionMode => PlayerMode == PlayerMode.Competition;
[NotMapped] public bool IsPracticeMode => PlayerMode == PlayerMode.Practice;
}
11 changes: 5 additions & 6 deletions src/Gameboard.Api/Features/Certificates/CertificatesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,10 @@ public async Task<IEnumerable<CompetitiveModeCertificate>> GetCompetitiveCertifi
.WithNoTracking<Data.Player>()
.Where
(
p => p.UserId == userId &&
p.SessionEnd > DateTimeOffset.MinValue &&
p.Game.PlayerMode == PlayerMode.Competition &&
(p.Game.GameEnd < now || p.Game.GameEnd == DateTimeOffset.MinValue) &&
p.Game.CertificateTemplateId != null
p =>
p.UserId == userId
&& (p.Game.GameEnd < now || p.Game.GameEnd == DateTimeOffset.MinValue)
&& p.Game.CertificateTemplateId != null
)
.WhereDateIsNotEmpty(p => p.SessionEnd)
.Where(p => p.Challenges.All(c => c.PlayerMode == PlayerMode.Competition))
Expand Down Expand Up @@ -159,7 +158,7 @@ public async Task<IEnumerable<CompetitiveModeCertificate>> GetCompetitiveCertifi
MaxPossibleScore = t.Game.MaxPossibleScore
},
Date = t.Game.GameEnd.IsEmpty() ? captain.SessionEnd : t.Game.GameEnd,
Rank = score.Rank > 0 ? score.Rank : null,
Rank = score?.Rank ?? 0,
Score = score is not null ? score.ScoreOverall : 0,
Duration = TimeSpan.FromMilliseconds(t.Time),
UniquePlayerCount = participationCounts?.UniquePlayerCount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public async Task<Challenge> Create(NewChallenge model, string actorId, string g
}

// if we're outside the execution window, we need to be sure the acting person is an admin
if (player.Game.IsCompetitionMode)
if (player.Game.PlayerMode == PlayerMode.Competition)
{
// check gamespace limits for competitive games only
var teamActiveChallenges = await _teamService.GetChallengesWithActiveGamespace(player.TeamId, player.GameId, cancellationToken);
Expand Down Expand Up @@ -156,7 +156,7 @@ public async Task<Challenge> Create(NewChallenge model, string actorId, string g
.SingleAsync(s => s.Id == model.SpecId, cancellationToken);

var playerCount = 1;
if (player.Game.AllowTeam)
if (player.Game.MaxTeamSize > 1)
{
playerCount = await _store
.WithNoTracking<Data.Player>()
Expand Down
1 change: 0 additions & 1 deletion src/Gameboard.Api/Features/Game/GameController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,6 @@ public async Task Rerank([FromRoute] string id, CancellationToken cancellationTo
await Authorize(_permissionsService.Can(PermissionKey.Scores_RegradeAndRerank));
await Validate(new Entity { Id = id });

await GameService.ReRank(id);
await _scoreDenormalization.DenormalizeGame(id, cancellationToken);
await _mediator.Publish(new GameCacheInvalidateNotification(id), cancellationToken);
}
Expand Down
29 changes: 0 additions & 29 deletions src/Gameboard.Api/Features/Game/GameService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ public interface IGameService
Task<bool> IsUserPlaying(string gameId, string userId);
Task<IEnumerable<Game>> List(GameSearchFilter model = null, bool sudo = false);
Task<GameGroup[]> ListGrouped(GameSearchFilter model, bool sudo);
Task ReRank(string id);
Task<Game> Retrieve(string id, bool accessHidden = true);
Task<ChallengeSpec[]> RetrieveChallengeSpecs(string id);
Task<SessionForecast[]> SessionForecast(string id);
Expand Down Expand Up @@ -213,34 +212,6 @@ public async Task UpdateImage(string id, string type, string filename)
await _store.SaveUpdate(entity, default);
}

public async Task ReRank(string id)
{
var players = await _store
.WithTracking<Data.Player>()
.Where(p => p.GameId == id && p.Mode == PlayerMode.Competition)
.OrderByDescending(p => p.Score)
.ThenBy(p => p.Time)
.ThenByDescending(p => p.CorrectCount)
.ThenByDescending(p => p.PartialCount)
.ToArrayAsync()
;

int rank = 0;
string last = "";
foreach (var player in players)
{
if (player.TeamId != last)
{
rank += 1;
last = player.TeamId;
}

player.Rank = rank;
}

await _store.SaveUpdateRange(players);
}

public Task<bool> IsUserPlaying(string gameId, string userId)
=> _store.AnyAsync<Data.Player>(p => p.GameId == gameId && p.UserId == userId, CancellationToken.None);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,38 +16,26 @@ namespace Gameboard.Api.Features.Scores;

public record GetScoreboardQuery(string GameId) : IRequest<ScoreboardData>;

internal class GetScoreboardHandler : IRequestHandler<GetScoreboardQuery, ScoreboardData>
internal class GetScoreboardHandler
(
IActingUserService actingUser,
EntityExistsValidator<GetScoreboardQuery, Data.Game> gameExists,
INowService now,
IScoreDenormalizationService scoringDenormalizationService,
IScoringService scoringService,
IStore store,
ITeamService teamService,
IValidatorService<GetScoreboardQuery> validatorService
) : IRequestHandler<GetScoreboardQuery, ScoreboardData>
{
private readonly IActingUserService _actingUser;
private readonly EntityExistsValidator<GetScoreboardQuery, Data.Game> _gameExists;
private readonly INowService _now;
private readonly IScoreDenormalizationService _scoringDenormalizationService;
private readonly IScoringService _scoringService;
private readonly IStore _store;
private readonly ITeamService _teamService;
private readonly IValidatorService<GetScoreboardQuery> _validatorService;

public GetScoreboardHandler
(
IActingUserService actingUser,
EntityExistsValidator<GetScoreboardQuery, Data.Game> gameExists,
INowService now,
IScoreDenormalizationService scoringDenormalizationService,
IScoringService scoringService,
IStore store,
ITeamService teamService,
IValidatorService<GetScoreboardQuery> validatorService
)
{
_actingUser = actingUser;
_gameExists = gameExists;
_now = now;
_scoringDenormalizationService = scoringDenormalizationService;
_scoringService = scoringService;
_store = store;
_teamService = teamService;
_validatorService = validatorService;
}
private readonly IActingUserService _actingUser = actingUser;
private readonly EntityExistsValidator<GetScoreboardQuery, Data.Game> _gameExists = gameExists;
private readonly INowService _now = now;
private readonly IScoreDenormalizationService _scoringDenormalizationService = scoringDenormalizationService;
private readonly IScoringService _scoringService = scoringService;
private readonly IStore _store = store;
private readonly ITeamService _teamService = teamService;
private readonly IValidatorService<GetScoreboardQuery> _validatorService = validatorService;

public async Task<ScoreboardData> Handle(GetScoreboardQuery request, CancellationToken cancellationToken)
{
Expand All @@ -63,17 +51,6 @@ await _validatorService

var teams = await LoadDenormalizedTeams(request.GameId, cancellationToken);

// if the teams aren't in the denormalized table, it's probably because this is an older game
// that denormalized data hasn't been generated for yet. Do it here:
if (!teams.Any())
{
// force the game to rerank
await _scoringDenormalizationService.DenormalizeGame(request.GameId, cancellationToken);

// then pull teams again
teams = await LoadDenormalizedTeams(request.GameId, cancellationToken);
}

// we grab competitive players only becaause later we filter out teams that have no competitive-mode players
// (since the scoreboard isn't really for practice mode)
var teamPlayers = await _store
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ private async Task RerankGame(string gameId)
.ToArrayAsync();

var captains = await _teamService.ResolveCaptains(teams.Select(t => t.TeamId).ToArray(), cancellationToken: CancellationToken.None);

var rankedTeams = _scoringService.GetTeamRanks(teams.Select(t =>
{
captains.TryGetValue(t.TeamId, out var captain);
Expand Down
6 changes: 6 additions & 0 deletions src/Gameboard.Api/Features/Support/SupportModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ public sealed class UpsertSupportSettingsAutoTagRequest
public bool? IsEnabled { get; set; }
public required string Tag { get; set; }
}

public static class TicketStatus
{
public static readonly string Closed = "Closed";
public static readonly string Open = "Open";
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
using System.Threading;
using System.Threading.Tasks;
using Gameboard.Api.Services;
using Gameboard.Api.Features.Scores;
using MediatR;

namespace Gameboard.Api.Features.Teams;

public record TeamSessionResetNotification(string GameId, string TeamId) : INotification;

internal class TeamSessionResetHandler : INotificationHandler<TeamSessionResetNotification>
internal class TeamSessionResetHandler(IScoreDenormalizationService scoreDenorm) : INotificationHandler<TeamSessionResetNotification>
{
private readonly GameService _gameService;

public TeamSessionResetHandler
(
GameService gameService
)
{
_gameService = gameService;
}
private readonly IScoreDenormalizationService _scoreDenorm = scoreDenorm;

public async Task Handle(TeamSessionResetNotification notification, CancellationToken cancellationToken)
{
await _gameService.ReRank(notification.GameId);
await _scoreDenorm.DenormalizeGame(notification.GameId, cancellationToken);
}
}
Loading