/// <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);
            }
        }
Exemple #2
0
        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);
            }
        }
Exemple #5
0
        /// <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.");
                    }
                }
            }
        }
Exemple #6
0
        /// <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);
            }
        }