/// <summary> /// This routine stores in the database any annotations the requester has uploaded. /// </summary> private async Task StoreAnnotations(DecodedSyncRequest request, SyncResponse response, int puzzleId, int teamId) { if (request.AnnotationRequests == null) { return; } foreach (var annotationRequest in request.AnnotationRequests) { await InsertOrUpdateOneAnnotation(response, puzzleId, teamId, annotationRequest.key, annotationRequest.contents); } }
public async Task <Dictionary <string, object> > GetSyncResponse(int eventId, int teamId, int puzzleId, List <int> query_puzzle_ids, int?min_solve_count, string annotations, string last_sync_time) { SyncResponse response = new SyncResponse(); // Get information for the puzzle that's being sync'ed. Puzzle thisPuzzle = await context.Puzzles.Where(puz => puz.ID == puzzleId).FirstOrDefaultAsync(); if (thisPuzzle == null) { response.AddError("Could not find the puzzle you tried to sync."); return(response.GetResult()); } // Check to make sure that they're not trying to sync a puzzle and event that don't go together. // That could be a security issue, allowing them to unlock pieces for a puzzle using the progress // they made in a whole different event! if (thisPuzzle.Event.ID != eventId) { response.AddError("That puzzle doesn't belong to that event."); return(response.GetResult()); } // Get the site version for this puzzle, so that if it's changed since the last time the // requester sync'ed, the requester will know to reload itself. response.SetSiteVersion(GetSiteVersion(thisPuzzle)); // Decode the request. If there are any errors, return an error response. DecodedSyncRequest request = new DecodedSyncRequest(query_puzzle_ids, min_solve_count, annotations, last_sync_time, thisPuzzle.MaxAnnotationKey, ref response); // Do any processing that requires fetching the list of all puzzles this team has // solved. Pass thisPuzzle.Group as the groupExcludedFromSolveCount parameter so that, // when counting solves, we don't count solves in the same group as this puzzle as part // of the solve count. await HandleSyncAspectsRequiringListOfSolvedPuzzles(request, response, thisPuzzle.Group, teamId, eventId); // Store any annotations the requester provided await StoreAnnotations(request, response, thisPuzzle.ID, teamId); // Fetch and return any annotations that the requester may not have yet await GetAnnotationsRequesterLacks(request, response, thisPuzzle.ID, teamId); return(response.GetResult()); }
/// <summary> /// This routine fetches the list of annotations that the requester's team has made since the last /// time the requester got a list of annotations. /// </summary> private async Task GetAnnotationsRequesterLacks(DecodedSyncRequest request, SyncResponse response, int puzzleId, int teamId) { // We get the current time (which we'll later set as the "sync time") *before* we do the // query, so that it's a conservative estimate of when the sync happened. DateTime now = DateTime.Now; List <Annotation> annotations; if (request.LastSyncTime == null) { // If the requester didn't specify a last-sync time, then provide all the annotations from their team // for this puzzle. annotations = await(from a in context.Annotations where a.PuzzleID == puzzleId && a.TeamID == teamId select a).ToListAsync(); } else { // We know the time of the last request, so in theory we should just return only // annotations that are from that time or later. But, it could happen that an // update that was concurrent with the previous sync request didn't make it into the // returned list but nevertheless got a timestamp after that sync request. Also, // there may be multiple web servers, with slightly unsynchronized clocks. So, for // safety, we subtract five seconds. This may cause us to unnecessarily fetch and // return an annotation the requester already knows about, but this is harmless: The // requester will see that the annotation has the same version number as the one // the requester already knows about for that key, and ignore it. var lastSyncTimeMinusSlop = request.LastSyncTime.Value.AddSeconds(-5); annotations = await(from a in context.Annotations where a.PuzzleID == puzzleId && a.TeamID == teamId && a.Timestamp >= lastSyncTimeMinusSlop select a).ToListAsync(); } response.SetSyncTimeAndAnnotations(now, annotations); }
/// <summary> /// This routine handles sync request aspects that require fetching a list of all the puzzles the team /// has solved. /// </summary> private async Task HandleSyncAspectsRequiringListOfSolvedPuzzles(DecodedSyncRequest request, SyncResponse response, string puzzleGroup, int teamId, int eventId, int puzzleId) { // If the requester isn't asking for pieces (by setting MinSolveCount to null), and isn't asking about // whether any puzzle IDs are solved, we can save time by not querying the list of solved puzzles. if (request.MinSolveCount == null && (request.QueryPuzzleIds == null || request.QueryPuzzleIds.Count == 0)) { return; } // If the request is asking which of the puzzle IDs in request.QueryPuzzleIds has been // solved, create a HashSet so that we can quickly look up if an ID is in that list. HashSet <int> queryPuzzleIdSet = null; if (request.QueryPuzzleIds != null) { queryPuzzleIdSet = new HashSet <int>(); foreach (var queryPuzzleId in request.QueryPuzzleIds) { queryPuzzleIdSet.Add(queryPuzzleId); } } // Get a list of all the puzzles this team has solved from this event. List <Puzzle> solves = await(from state in context.PuzzleStatePerTeam where state.TeamID == teamId && state.SolvedTime != null select state.Puzzle).ToListAsync(); int maxSolveCount = 0; List <int> solvedPuzzles = new List <int>(); foreach (var solvedPuzzle in solves) { // If the request is asking whether certain puzzles are solved, check if it's // in the set and, if so, put it in the list of solved puzzles to inform the // requester of. Also, if we've been asked to query all puzzles in the puzzle's // group and this is one of them, put it in the list of solved puzzles. if (queryPuzzleIdSet != null && queryPuzzleIdSet.Contains(solvedPuzzle.ID)) { solvedPuzzles.Add(solvedPuzzle.ID); } else if (request.QueryAllInGroup && solvedPuzzle.Group == puzzleGroup) { solvedPuzzles.Add(solvedPuzzle.ID); } // When counting solves, only count puzzles if they're not in the same group // as the puzzle being synced, and only if they're worth at least 10 points // and exactly 0 hint coins. if (puzzleGroup == null || solvedPuzzle.Group != puzzleGroup) { if (solvedPuzzle.SolveValue >= 10 && solvedPuzzle.HintCoinsForSolve == 0) { maxSolveCount += 1; } } // If the user solved a cheat code in this group, treat it as a solve count of 1000. // The reason we require the cheat code to be in the same group as the puzzle being // sync'ed is to defend against mistakes by authors in other groups. If an author // of a puzzle in another group accidentally sets the cheat-code flag on their // puzzle, we don't want to consequently give all teams that solve it all pieces of // the puzzle being sync'ed. if (solvedPuzzle.IsCheatCode && solvedPuzzle.Group == puzzleGroup) { maxSolveCount += 1000; } } // If the requester is asking for puzzle pieces (by setting request.MinSolveCount != null) // and if there are pieces of the puzzle that the requester has now earned but hasn't // yet received, because the maximum solve count they've earned is at least as high // as the minimum solve count the requester is asking for, then return those pieces. // Note that request.MinSolveCount is the minimum solve count of tokens the requester // *hasn't* seen yet. if (request.MinSolveCount != null && maxSolveCount >= request.MinSolveCount) { List <Piece> pieces = await(from piece in context.Pieces where piece.ProgressLevel >= request.MinSolveCount && piece.ProgressLevel <= maxSolveCount && piece.PuzzleID == puzzleId select piece).ToListAsync(); response.SetMinAndMaxSolveCountAndPieces(request.MinSolveCount.Value, maxSolveCount, pieces); } // If we found some solved puzzles in request.QueryPuzzleIds, return those in the // response. if (solvedPuzzles.Count > 0) { response.SetSolvedPuzzles(solvedPuzzles); } }
/// <summary> /// This routine stores in the database any annotations the requester has uploaded. /// </summary> private async Task StoreAnnotations(DecodedSyncRequest request, SyncResponse response, int puzzleId, int teamId) { if (request.AnnotationRequests == null) { return; } foreach (var annotationRequest in request.AnnotationRequests) { // Try to generate this as a new annotation, with version 1. Annotation annotation = new Annotation(); annotation.PuzzleID = puzzleId; annotation.TeamID = teamId; annotation.Key = annotationRequest.key; annotation.Version = 1; annotation.Contents = annotationRequest.contents; annotation.Timestamp = DateTime.Now; try { context.Annotations.Add(annotation); await context.SaveChangesAsync(); } catch (DbUpdateException) { // If the insert fails, there must already be an annotation there with the // same puzzle ID, team ID, and key. So we need to update the existing one. // As we do so, we increment its version number and update its timestamp. // // You may wonder why we're using ExecuteSqlCommandAsync instead of "normal" // Entity Framework database functions. The answer is that we need to atomically // update the Version field of the record, and Entity Framework has no way of // expressing that directly. // // The reason we want to update the version number atomically is that we rely // on the version number being a unique identifier of an annotation. We don't // want the following scenario: // // Alice tries to set the annotation for key 17 to A, and simultaneously Bob // tries to set it to B. Each reads the current version number, finds it to be // 3, and updates the annotation to have version 4. Both of these updates may // succeed, but one will overwrite the other; let's say Bob's write happens last // and "wins". So Alice may believe that version 4 is A when actually version 4 // is B. When Alice asks for the current version, she'll be told it's version 4, // and Alice will believe this means it's A. So Alice will believe that A is // what's stored in the database even though it's not. Alice and Bob's computers // will display different annotations for the same key, indefinitely. // // Note that we need a version number because the timestamp isn't guaranteed to // be unique. So in the example above Alice and Bob might wind up updating with // the same timestamp. // // You may also wonder why we use DateTime.Now instead of letting the database // assign the timestamp itself. The reason is that the database might be running // on a different machine than the puzzle server, and it might be using a different // time zone. // First, detach the annotation from the context so the context doesn't think the annotation is in the database. context.Entry(annotation).State = EntityState.Detached; try { var sqlCommand = "UPDATE Annotations SET Version = Version + 1, Contents = @Contents, Timestamp = @Timestamp WHERE PuzzleID = @PuzzleID AND TeamID = @TeamID AND [Key] = @Key"; int result = await context.Database.ExecuteSqlCommandAsync(sqlCommand, new SqlParameter("@Contents", annotationRequest.contents), new SqlParameter("@Timestamp", DateTime.Now), new SqlParameter("@PuzzleID", puzzleId), new SqlParameter("@TeamID", teamId), new SqlParameter("@Key", annotationRequest.key)); if (result != 1) { response.AddError("Annotation update failed."); } } catch (DbUpdateException) { response.AddError("Encountered error while trying to update annotation."); } catch (Exception) { response.AddError("Miscellaneous error while trying to update annotation."); } } } }
/// <summary> /// This routine handles sync request aspects that require fetching a list of all the puzzles the team /// has solved. /// </summary> private async Task HandleSyncAspectsRequiringListOfSolvedPuzzles(DecodedSyncRequest request, SyncResponse response, string groupExcludedFromSolveCount, int teamId, int eventId) { // If the requester isn't asking for pieces (by setting MinSolveCount to null), and isn't asking about // whether any puzzle IDs are solved, we can save time by not querying the list of solved puzzles. if (request.MinSolveCount == null && (request.QueryPuzzleIds == null || request.QueryPuzzleIds.Count == 0)) { return; } // If the request is asking which of the puzzle IDs in request.QueryPuzzleIds has been // solved, create a HashSet so that we can quickly look up if an ID is in that list. HashSet <int> queryPuzzleIdSet = null; if (request.QueryPuzzleIds != null) { queryPuzzleIdSet = new HashSet <int>(); foreach (var queryPuzzleId in request.QueryPuzzleIds) { queryPuzzleIdSet.Add(queryPuzzleId); } } // Get a list of all the puzzles this team has solved from this event. List <Puzzle> solves = await(from state in context.PuzzleStatePerTeam where state.TeamID == teamId && state.SolvedTime != null && state.Puzzle.Event.ID == eventId select state.Puzzle).ToListAsync(); int maxSolveCount = 0; List <int> solvedPuzzles = new List <int>(); foreach (var solvedPuzzle in solves) { // If the request is asking whether certain puzzles are solved, check if it's // in the set and, if so, put it in the list of solved puzzles to inform the // requester of. if (queryPuzzleIdSet != null && queryPuzzleIdSet.Contains(solvedPuzzle.ID)) { solvedPuzzles.Add(solvedPuzzle.ID); } // When counting solves, only count puzzles if they're not in the group that doesn't // count toward the solve count, and only if they're worth at least 10 points. if (groupExcludedFromSolveCount == null || solvedPuzzle.Group != groupExcludedFromSolveCount) { if (solvedPuzzle.SolveValue >= 10) { maxSolveCount += 1; } } // If the user solved a cheat code, treat it as a solve count of 1000. if (solvedPuzzle.IsCheatCode) { maxSolveCount += 1000; } } // If the requester is asking for puzzle pieces (by setting request.MinSolveCount != null) // and if there are pieces of the puzzle that the requester has now earned but hasn't // yet received, because the maximum solve count they've earned is at least as high // as the minimum solve count the requester is asking for, then return those pieces. // Note that request.MinSolveCount is the minimum solve count of tokens the requester // *hasn't* seen yet. if (request.MinSolveCount != null && maxSolveCount >= request.MinSolveCount) { List <Piece> pieces = await(from piece in context.Pieces where piece.ProgressLevel >= request.MinSolveCount && piece.ProgressLevel <= maxSolveCount select piece).ToListAsync(); response.SetMinAndMaxSolveCountAndPieces(request.MinSolveCount.Value, maxSolveCount, pieces); } // If we found some solved puzzles in request.QueryPuzzleIds, return those in the // response. if (solvedPuzzles.Count > 0) { response.SetSolvedPuzzles(solvedPuzzles); } }