public static Scaffold.Command JsonToModel(JSON.Command command, short worldId, int accessGroupId, Scaffold.Command existingCommand, DateTime currentTime, Scaffold.VaultContext context = null)
        {
            if (command == null)
            {
                if (existingCommand != null && context != null)
                {
                    context.Remove(existingCommand);
                }

                return(null);
            }

            Scaffold.Command result;
            if (existingCommand != null)
            {
                result = existingCommand;
            }
            else
            {
                result               = new Scaffold.Command();
                result.CommandId     = command.CommandId.Value;
                result.WorldId       = worldId;
                result.AccessGroupId = accessGroupId;
                if (context != null)
                {
                    context.Add(result);
                }
            }

            result.CommandId       = command.CommandId.Value;
            result.UserLabel       = result.UserLabel ?? command.UserLabel;  // Don't overwrite existing labels
            result.SourcePlayerId  = command.SourcePlayerId.Value;
            result.SourceVillageId = command.SourceVillageId.Value;
            result.TargetPlayerId  = command.TargetPlayerId;
            result.TargetVillageId = command.TargetVillageId.Value;
            result.LandsAt         = command.LandsAt.Value;
            result.FirstSeenAt     = result.FirstSeenAt == DateTime.MinValue ? currentTime : result.FirstSeenAt;
            result.IsAttack        = command.CommandType == JSON.CommandType.Attack;
            result.IsReturning     = command.IsReturning.Value;

            result.Army = ArmyConvert.JsonToArmy(command.Troops, worldId, result.Army, context);

            if (result.TroopType == null)
            {
                result.TroopType = TroopTypeConvert.TroopTypeToString(command.TroopType);
            }
            else if (command.TroopType != null)
            {
                var currentType = result.TroopType.ToTroopType();
                var updatedType = command.TroopType.Value;

                if (Native.ArmyStats.TravelSpeed[currentType] < Native.ArmyStats.TravelSpeed[updatedType])
                {
                    result.TroopType = updatedType.ToTroopString();
                }
            }

            return(result);
        }
        public static JSON.Command ModelToJson(Scaffold.Command command)
        {
            var result = new JSON.Command();

            result.CommandId       = command.CommandId;
            result.UserLabel       = command.UserLabel;
            result.SourcePlayerId  = command.SourcePlayerId;
            result.SourceVillageId = command.SourceVillageId;
            result.TargetPlayerId  = command.TargetPlayerId;
            result.TargetVillageId = command.TargetVillageId;
            result.LandsAt         = command.LandsAt;
            result.CommandType     = command.IsAttack ? JSON.CommandType.Attack : JSON.CommandType.Support;
            result.IsReturning     = command.IsReturning;

            result.TroopType = TroopTypeConvert.StringToTroopType(command.TroopType);
            result.Troops    = ArmyConvert.ArmyToJson(command.Army);

            return(result);
        }
 public static JSON.Command ToJson(this Scaffold.Command command) =>
 ModelToJson(command);
 public static Scaffold.Command ToModel(this JSON.Command command, short worldId, int accessGroupId, Scaffold.Command existingCommand, DateTime currentTime, Scaffold.VaultContext context = null) =>
 JsonToModel(command, worldId, accessGroupId, existingCommand, currentTime, context);
        public async Task <IActionResult> GetBacktimePlan()
        {
            var serverTime = CurrentServerTime;

            var invalidTribeIds = await(
                from tribe in CurrentSets.Ally
                join player in CurrentSets.Player on tribe.TribeId equals player.TribeId
                join user in CurrentSets.User on player.PlayerId equals user.PlayerId
                where user.Enabled
                select tribe.TribeId
                ).Distinct().ToListAsync();

            var invalidPlayerIds = await(
                from player in CurrentSets.Player
                join user in CurrentSets.User on player.PlayerId equals user.PlayerId into user
                where user.Any(u => u.Enabled) || (player.TribeId != null && invalidTribeIds.Contains(player.TribeId.Value))
                select player.PlayerId
                ).ToListAsync();

            var invalidVillageIds = await(
                from village in CurrentSets.Village
                where village.PlayerId != null && invalidPlayerIds.Contains(village.PlayerId.Value)
                select village.VillageId
                ).ToListAsync();

            var ownVillages = await(
                from village in CurrentSets.Village
                join currentVillage in CurrentSets.CurrentVillage.Include(cv => cv.ArmyAtHome) on village.VillageId equals currentVillage.VillageId
                where currentVillage.ArmyAtHome != null
                where village.PlayerId == CurrentPlayerId
                select new { village, currentVillage }
                ).ToDictionaryAsync(d => d.village, d => d.currentVillage);

            var possibleCommands = await(
                from command in CurrentSets.Command
                .Include(c => c.Army)
                .Include(c => c.SourceVillage)
                where command.ArmyId != null
                where command.ReturnsAt > serverTime
                where !invalidVillageIds.Contains(command.SourceVillageId)
                select command
                ).ToListAsync();

            var offensiveTypes  = new[] { JSON.TroopType.Axe, JSON.TroopType.Light, JSON.TroopType.Heavy, JSON.TroopType.Ram, JSON.TroopType.Catapult };
            var allInstructions = new ConcurrentDictionary <Scaffold.Command, List <Planning.CommandInstruction> >();

            var ownVillagesById = ownVillages.Keys.ToDictionary(v => v.VillageId, v => v);

            bool MeetsMinimumPopulation(Scaffold.Command command)
            {
                var army = (JSON.Army)command.Army;

                return(army != null && 2000 < Model.Native.ArmyStats.CalculateTotalPopulation(army, offensiveTypes));
            }

            var targetPlayerIdsTmp = new ConcurrentDictionary <long, byte>();

            //  Generate backtime plans
            try
            {
                var commands        = possibleCommands.Where(MeetsMinimumPopulation);
                var parallelOptions = new ParallelOptions {
                    MaxDegreeOfParallelism = Environment.ProcessorCount
                };
                var planningTask = Parallel.ForEach(commands, parallelOptions, (command) =>
                {
                    var planner = new Planning.CommandOptionsCalculator(CurrentWorldSettings);
                    planner.Requirements.Add(new Planning.Requirements.MaximumTravelTimeRequirement
                    {
                        MaximumTime = command.ReturnsAt.Value - serverTime
                    });

                    planner.Requirements.Add(Planning.Requirements.MinimumOffenseRequirement.FractionalNuke(0.05f).LimitTroopType(offensiveTypes));
                    planner.Requirements.Add(Planning.Requirements.WithoutTroopTypeRequirement.WithoutNobles);

                    var plan = planner.GenerateOptions(ownVillages, command.SourceVillage);
                    if (plan.Count > 0)
                    {
                        targetPlayerIdsTmp.TryAdd(command.SourcePlayerId, 0);
                        allInstructions.TryAdd(command, plan);
                    }
                });
            }
            catch (AggregateException e)
            {
                throw e.InnerException ?? e.InnerExceptions.First();
            }

            //  Find existing backtimes for commands that had plans generated
            var backtimedVillageIds         = allInstructions.Keys.Select(c => c.SourceVillageId).ToList();
            var commandsToBacktimedVillages = await(
                from command in CurrentSets.Command
                where backtimedVillageIds.Contains(command.TargetVillageId)
                where command.Army != null
                where command.LandsAt > serverTime
                select new { command.TargetVillageId, command.LandsAt, command.Army }
                ).ToListAsync();

            var troopsAtBacktimedVillages = await(
                from village in CurrentSets.CurrentVillage
                where backtimedVillageIds.Contains(village.VillageId)
                select new { village.VillageId, village.ArmyStationed }
                ).ToDictionaryAsync(v => v.VillageId, v => v.ArmyStationed);

            var battleSimulator             = new Features.Simulation.BattleSimulator();
            var existingBacktimesPerCommand = allInstructions.Keys.ToDictionary(c => c.CommandId, id => 0);

            foreach (var command in commandsToBacktimedVillages)
            {
                Scaffold.Command backtimedCommand = null;
                var commandsReturningToVillage    = allInstructions.Keys.Where(c => c.SourceVillageId == command.TargetVillageId);
                foreach (var returning in commandsReturningToVillage)
                {
                    if (command.LandsAt > returning.ReturnsAt && (command.LandsAt - returning.ReturnsAt.Value).TotalSeconds < 10)
                    {
                        backtimedCommand = returning;
                    }
                }

                if (backtimedCommand == null)
                {
                    continue;
                }

                var backtimedArmy      = (JSON.Army)backtimedCommand.Army;
                var battleResult       = battleSimulator.SimulateAttack(command.Army, backtimedArmy, 20, CurrentWorldSettings.ArchersEnabled);
                var originalPopulation = (float)Model.Native.ArmyStats.CalculateTotalPopulation(backtimedArmy, offensiveTypes);
                var newPopulation      = (float)Model.Native.ArmyStats.CalculateTotalPopulation(battleResult.DefendingArmy, offensiveTypes);

                var percentLost = 1 - newPopulation / originalPopulation;
                if (percentLost > 0.85f)
                {
                    existingBacktimesPerCommand[backtimedCommand.CommandId]++;
                }
            }

            var targetPlayerIds = targetPlayerIdsTmp.Keys.ToList();

            var playerInfoById = await(
                from player in CurrentSets.Player
                where targetPlayerIds.Contains(player.PlayerId)
                select new { player.PlayerId, player.TribeId, player.PlayerName }
                ).ToDictionaryAsync(i => i.PlayerId, i => i);

            var tribeIds      = playerInfoById.Values.Select(i => i.TribeId).Where(t => t != null).Distinct();
            var tribeInfoById = await(
                from tribe in CurrentSets.Ally
                where tribeIds.Contains(tribe.TribeId)
                select new { tribe.TribeId, tribe.Tag, tribe.TribeName }
                ).ToDictionaryAsync(i => i.TribeId, i => i);

            //  Get JSON version of instructions and info for all backtimeable nukes
            var result = allInstructions.Select(commandInstructions =>
            {
                (var command, var instructions) = commandInstructions.Tupled();

                var backtimeInfo  = new JSON.BacktimeInfo();
                var targetVillage = command.SourceVillage; // "Target" is the source of the command
                var targetPlayer  = playerInfoById[command.SourcePlayerId];
                var targetTribe   = targetPlayer.TribeId == null ? null : tribeInfoById[targetPlayer.TribeId.Value];
                var isStacked     = false;
                if (troopsAtBacktimedVillages[targetVillage.VillageId] != null)
                {
                    var stationedTroops = troopsAtBacktimedVillages[targetVillage.VillageId];
                    var defensePower    = Features.Simulation.BattleSimulator.TotalDefensePower(stationedTroops);
                    isStacked           = defensePower > 1000000;
                }

                //  Gather instructions and info for this command
                return(new JSON.BacktimeInfo
                {
                    //  General info
                    TravelingArmyPopulation = Model.Native.ArmyStats.CalculateTotalPopulation(command.Army, offensiveTypes),
                    TargetPlayerName = targetPlayer.PlayerName.UrlDecode(),
                    TargetTribeName = targetTribe?.TribeName?.UrlDecode(),
                    TargetTribeTag = targetTribe?.Tag?.UrlDecode(),
                    ExistingBacktimes = existingBacktimesPerCommand[command.CommandId],
                    IsStacked = isStacked,

                    //  Convert backtime instructions to JSON format
                    Instructions = instructions.Select(instruction =>
                    {
                        var sourceVillage = ownVillagesById[instruction.SendFrom];
                        var sourceVillageArmy = (JSON.Army)ownVillages[sourceVillage].ArmyAtHome;
                        var instructionArmy = sourceVillageArmy.BasedOn(instruction.TroopType);

                        return new JSON.BattlePlanCommand
                        {
                            LandsAt = command.ReturnsAt.Value,
                            LaunchAt = command.ReturnsAt.Value - instruction.TravelTime,
                            TravelTimeSeconds = (int)instruction.TravelTime.TotalSeconds,

                            TroopType = instruction.TroopType.ToString().ToLower(),
                            CommandPopulation = Model.Native.ArmyStats.CalculateTotalPopulation(instructionArmy),
                            CommandAttackPower = Features.Simulation.BattleSimulator.TotalAttackPower(instructionArmy),
                            CommandDefensePower = Features.Simulation.BattleSimulator.TotalDefensePower(instructionArmy),

                            SourceVillageId = instruction.SendFrom,
                            TargetVillageId = instruction.SendTo,

                            SourceVillageName = sourceVillage.VillageName.UrlDecode(),
                            TargetVillageName = targetVillage.VillageName.UrlDecode(),

                            SourceVillageX = sourceVillage.X.Value,
                            SourceVillageY = sourceVillage.Y.Value,

                            TargetVillageX = targetVillage.X.Value,
                            TargetVillageY = targetVillage.Y.Value,
                        };
                    }).ToList()
                });
            }).ToList();

            return(Ok(result));
        }
示例#6
0
        public async Task <IActionResult> Post([FromBody] JSON.Report jsonReport)
        {
            if (ModelState.IsValid)
            {
                if (!Configuration.Security.ReportIgnoreExpectedPopulationBounds &&
                    !ArmyValidate.MeetsPopulationRestrictions(jsonReport.AttackingArmy))
                {
                    context.InvalidDataRecord.Add(MakeInvalidDataRecord(
                                                      JsonConvert.SerializeObject(jsonReport),
                                                      "Troops in attacking army exceed possible village population"
                                                      ));
                    return(BadRequest());
                }

                if (!Configuration.Security.ReportIgnoreExpectedPopulationBounds &&
                    !ArmyValidate.MeetsPopulationRestrictions(jsonReport.TravelingTroops))
                {
                    context.InvalidDataRecord.Add(MakeInvalidDataRecord(
                                                      JsonConvert.SerializeObject(jsonReport),
                                                      "Troops in traveling army exceed possible village population"
                                                      ));
                }

                if (jsonReport.OccurredAt.Value > CurrentServerTime)
                {
                    context.InvalidDataRecord.Add(MakeInvalidDataRecord(
                                                      JsonConvert.SerializeObject(jsonReport),
                                                      "The report 'OccurredAt' is in the future"
                                                      ));
                    //  Return 200/OK to trick malicious actors
                    return(Ok());
                }

                bool isDuplicate    = false;
                var  scaffoldReport = await Profile("Find existing report by ID", () => (
                                                        from report in CurrentSets.Report.IncludeReportData()
                                                        where report.ReportId == jsonReport.ReportId.Value
                                                        select report
                                                        ).FirstOrDefaultAsync()
                                                    );

                if (scaffoldReport == null)
                {
                    await Profile("Find existing report by contents", async() =>
                    {
                        var reportsMatchingDetails = await(
                            from report in CurrentSets.Report.IncludeReportData()
                            where report.OccuredAt == jsonReport.OccurredAt
                            where report.AttackerPlayerId == jsonReport.AttackingPlayerId
                            where report.AttackerVillageId == jsonReport.AttackingVillageId
                            where report.DefenderPlayerId == jsonReport.DefendingPlayerId
                            where report.DefenderVillageId == jsonReport.DefendingVillageId
                            select report
                            ).ToListAsync();

                        var existingDuplicate = reportsMatchingDetails.FirstOrDefault((r) =>
                                                                                      jsonReport.AttackingArmy == r.AttackerArmy &&
                                                                                      jsonReport.DefendingArmy == r.DefenderArmy &&
                                                                                      jsonReport.AttackingArmyLosses == r.AttackerLossesArmy &&
                                                                                      jsonReport.DefendingArmyLosses == r.DefenderLossesArmy &&
                                                                                      jsonReport.TravelingTroops == r.DefenderTravelingArmy
                                                                                      );

                        isDuplicate = existingDuplicate != null;
                    });
                }

                var tx = BuildTransaction();
                context.Transaction.Add(tx);

                if (isDuplicate)
                {
                    var isIgnored = await context.IgnoredReport.AnyAsync(r => r.ReportId == jsonReport.ReportId.Value);

                    if (!isIgnored)
                    {
                        context.IgnoredReport.Add(new IgnoredReport
                        {
                            AccessGroupId = CurrentAccessGroupId,
                            ReportId      = jsonReport.ReportId.Value,
                            WorldId       = CurrentWorldId
                        });
                    }
                }
                else
                {
                    Profile("Populate scaffold report", () =>
                    {
                        if (scaffoldReport == null)
                        {
                            scaffoldReport               = new Scaffold.Report();
                            scaffoldReport.WorldId       = CurrentWorldId;
                            scaffoldReport.AccessGroupId = CurrentAccessGroupId;
                            context.Report.Add(scaffoldReport);
                        }
                        else
                        {
                            var existingJsonReport = ReportConvert.ModelToJson(scaffoldReport);

                            if (existingJsonReport != jsonReport && scaffoldReport.TxId.HasValue)
                            {
                                context.ConflictingDataRecord.Add(new Scaffold.ConflictingDataRecord
                                {
                                    ConflictingTx = tx,
                                    OldTxId       = scaffoldReport.TxId.Value
                                });
                            }
                        }

                        jsonReport.ToModel(CurrentWorldId, scaffoldReport, context);

                        scaffoldReport.Tx = tx;
                    });

                    if (jsonReport.AttackingPlayerId != null)
                    {
                        await Profile("Update command troop type", async() =>
                        {
                            var lostAllTroops = jsonReport.AttackingArmy == jsonReport.AttackingArmyLosses;

                            var command = await Model.UtilQuery.FindCommandForReport(scaffoldReport, context);

                            if (command == null && !lostAllTroops && (jsonReport.Loyalty == null || jsonReport.Loyalty > 0))
                            {
                                //  WARNING - This will auto-generate a command with a random ID,
                                //      if a new TW command is uploaded with the given ID any backtime
                                //      calculations for this old command will get screwy
                                try
                                {
                                    await context.SaveChangesAsync();
                                }
                                catch (Exception e)
                                {
                                    throw e;
                                }

                                command                 = new Scaffold.Command();
                                command.Tx              = tx;
                                command.WorldId         = CurrentWorldId;
                                command.AccessGroupId   = CurrentAccessGroupId;
                                command.IsReturning     = true;
                                command.FirstSeenAt     = CurrentServerTime;
                                command.IsAttack        = true;
                                command.SourcePlayerId  = jsonReport.AttackingPlayerId.Value;
                                command.TargetPlayerId  = jsonReport.DefendingPlayerId;
                                command.SourceVillageId = jsonReport.AttackingVillageId.Value;
                                command.TargetVillageId = jsonReport.DefendingVillageId.Value;
                                command.LandsAt         = jsonReport.OccurredAt.Value;

                                bool madeCommand = false;

                                //  Need to auto-generate a random command ID
                                while (!madeCommand)
                                {
                                    try
                                    {
                                        command.CommandId = Random.NextLong >> 14;
                                        context.Add(command);
                                        await context.SaveChangesAsync();
                                        madeCommand = true;
                                    }
                                    catch (Exception) { }
                                }
                            }

                            if (command != null)
                            {
                                JSON.TroopType?slowestType = null;
                                float slowestSpeed         = -1;
                                foreach (var troopType in jsonReport.AttackingArmy.Where(kvp => kvp.Value > 0).Select(kvp => kvp.Key))
                                {
                                    var travelSpeed = Native.ArmyStats.TravelSpeed[troopType];
                                    if (slowestType == null)
                                    {
                                        slowestType  = troopType;
                                        slowestSpeed = travelSpeed;
                                    }
                                    else if (travelSpeed > slowestSpeed)
                                    {
                                        slowestType  = troopType;
                                        slowestSpeed = travelSpeed;
                                    }
                                }

                                var attackingVillage = await CurrentSets.Village
                                                       .FromWorld(CurrentWorldId)
                                                       .Where(v => v.VillageId == jsonReport.AttackingVillageId)
                                                       .FirstOrDefaultAsync();

                                var defendingVillage = await CurrentSets.Village
                                                       .FromWorld(CurrentWorldId)
                                                       .Where(v => v.VillageId == jsonReport.DefendingVillageId)
                                                       .FirstOrDefaultAsync();

                                var travelCalculator = new Features.Simulation.TravelCalculator(CurrentWorldSettings.GameSpeed, CurrentWorldSettings.UnitSpeed);
                                var travelTime       = travelCalculator.CalculateTravelTime(slowestType.Value, attackingVillage, defendingVillage);

                                command.TroopType = slowestType.Value.ToTroopString();

                                command.Army = ArmyConvert.JsonToArmy(jsonReport.AttackingArmy - jsonReport.AttackingArmyLosses, CurrentWorldId, command.Army, context);
                                if (command.Army != null)
                                {
                                    command.Army.WorldId = CurrentWorldId;
                                }
                                command.ReturnsAt   = scaffoldReport.OccuredAt + travelTime;
                                command.IsReturning = true;
                            }
                        });
                    }
                }

                //if (jsonReport.Loyalty <= 0)
                //{
                //    var conquer = new Scaffold.Conquer
                //    {
                //        WorldId = CurrentWorldId,
                //        OldOwner = jsonReport.DefendingPlayerId,
                //        NewOwner = jsonReport.AttackingPlayerId,
                //        VillageId = jsonReport.DefendingVillageId,
                //        UnixTimestamp = new DateTimeOffset(jsonReport.OccurredAt.Value).ToUnixTimeSeconds()
                //    };

                //    context.Add(conquer);
                //}

                await Profile("Save changes", () => context.SaveChangesAsync());

                //  Run upload history update in separate query to prevent creating multiple history
                //  entries
                var userUploadHistory = await EFUtil.GetOrCreateUserUploadHistory(context, CurrentUserId);

                userUploadHistory.LastUploadedReportsAt = CurrentServerTime;
                await context.SaveChangesAsync();

                return(Ok());
            }
            else
            {
                return(BadRequest(ModelState));
            }
        }
        public async Task <IActionResult> Post([FromBody] JSON.ManyCommands jsonCommands)
        {
            if (ModelState.IsValid)
            {
                var mappedCommands = jsonCommands.Commands.ToDictionary(c => c.CommandId, c => c);
                var commandIds     = jsonCommands.Commands.Select(c => c.CommandId).ToList();

                var allVillageIds = jsonCommands.Commands
                                    .Select(c => c.SourceVillageId)
                                    .Concat(jsonCommands.Commands.Select(c => c.TargetVillageId))
                                    .Select(id => id.Value)
                                    .Distinct();

                var allCurrentVillageIds = await CurrentSets.CurrentVillage.Where(v => allVillageIds.Contains(v.VillageId)).Select(v => v.VillageId).ToListAsync();

                var allVillagesMissingCurrentEntries = allVillageIds.Except(allCurrentVillageIds).ToList();
                if (allVillagesMissingCurrentEntries.Count > 0)
                {
                    foreach (var id in allVillagesMissingCurrentEntries)
                    {
                        context.Add(new CurrentVillage
                        {
                            VillageId     = id,
                            AccessGroupId = CurrentAccessGroupId,
                            WorldId       = CurrentWorldId
                        });
                    }

                    await context.SaveChangesAsync();
                }

                var villageIdsFromCommandsMissingTroopType = jsonCommands.Commands
                                                             .Where(c => c.TroopType == null)
                                                             .SelectMany(c => new[] { c.SourceVillageId, c.TargetVillageId })
                                                             .Distinct()
                                                             .ToList();

                var scaffoldCommands = await Profile("Get existing commands", () => (
                                                         from command in CurrentSets.Command.IncludeCommandData()
                                                         where commandIds.Contains(command.CommandId)
                                                         select command
                                                         ).ToListAsync());

                var villageIdsFromCommandsMissingTroopTypes = await Profile("Get villages for commands missing troop type", () => (
                                                                                from village in CurrentSets.Village
                                                                                where villageIdsFromCommandsMissingTroopType.Contains(village.VillageId)
                                                                                select village
                                                                                ).ToListAsync());

                var allVillages = await Profile("Get all relevant villages", () => (
                                                    from village in CurrentSets.Village
                                                    where allVillageIds.Contains(village.VillageId)
                                                    select village
                                                    ).ToListAsync());

                var mappedScaffoldCommands = scaffoldCommands.ToDictionary(c => c.CommandId, c => c);
                var villagesById           = allVillages.ToDictionary(v => v.VillageId, v => v);

                var tx = BuildTransaction();
                context.Transaction.Add(tx);

                Profile("Generate scaffold commands", () =>
                {
                    foreach (var jsonCommand in jsonCommands.Commands)
                    {
                        if (!Configuration.Security.AllowCommandArrivalBeforeServerTime &&
                            jsonCommand.LandsAt.HasValue &&
                            jsonCommand.LandsAt.Value < CurrentServerTime)
                        {
                            context.InvalidDataRecord.Add(MakeInvalidDataRecord(
                                                              JsonConvert.SerializeObject(jsonCommand),
                                                              "Command.landsAt is earlier than current server time"
                                                              ));
                            continue;
                        }

                        if (!Configuration.Security.ReportIgnoreExpectedPopulationBounds &&
                            !ArmyValidate.MeetsPopulationRestrictions(jsonCommand.Troops))
                        {
                            context.InvalidDataRecord.Add(MakeInvalidDataRecord(
                                                              JsonConvert.SerializeObject(jsonCommand),
                                                              "Troops in command exceed possible village population"
                                                              ));
                            continue;
                        }

                        var scaffoldCommand = mappedScaffoldCommands.GetValueOrDefault(jsonCommand.CommandId.Value);
                        //  Don't process/update commands that are already "complete" (have proper army data attached to them)
                        if (scaffoldCommand?.Army != null)
                        {
                            continue;
                        }

                        var travelCalculator = new Features.Simulation.TravelCalculator(CurrentWorldSettings.GameSpeed, CurrentWorldSettings.UnitSpeed);
                        var timeRemaining    = jsonCommand.LandsAt.Value - CurrentServerTime;
                        var sourceVillage    = villagesById[jsonCommand.SourceVillageId.Value];
                        var targetVillage    = villagesById[jsonCommand.TargetVillageId.Value];
                        var estimatedType    = travelCalculator.EstimateTroopType(timeRemaining, sourceVillage, targetVillage);

                        if (jsonCommand.TroopType == null)
                        {
                            jsonCommand.TroopType = estimatedType;
                        }
                        else
                        {
                            var estimatedTravelSpeed = Native.ArmyStats.TravelSpeed[estimatedType];
                            var reportedTravelSpeed  = Native.ArmyStats.TravelSpeed[jsonCommand.TroopType.Value];

                            //  ie if command is tagged as "spy" but travel speed is effective for
                            //  rams
                            if (estimatedTravelSpeed > reportedTravelSpeed)
                            {
                                jsonCommand.TroopType = estimatedType;
                            }
                        }

                        if (scaffoldCommand == null)
                        {
                            scaffoldCommand               = new Scaffold.Command();
                            scaffoldCommand.World         = CurrentWorld;
                            scaffoldCommand.AccessGroupId = CurrentAccessGroupId;
                            jsonCommand.ToModel(CurrentWorldId, CurrentAccessGroupId, scaffoldCommand, CurrentServerTime, context);
                            context.Command.Add(scaffoldCommand);
                        }
                        else
                        {
                            var existingJsonCommand = CommandConvert.ModelToJson(scaffoldCommand);
                            if (existingJsonCommand.IsReturning == jsonCommand.IsReturning && existingJsonCommand != jsonCommand)
                            {
                                context.ConflictingDataRecord.Add(new Scaffold.ConflictingDataRecord
                                {
                                    OldTxId       = scaffoldCommand.TxId.Value,
                                    ConflictingTx = tx
                                });
                            }

                            jsonCommand.ToModel(CurrentWorldId, CurrentAccessGroupId, scaffoldCommand, CurrentServerTime, context);
                        }

                        if (String.IsNullOrWhiteSpace(scaffoldCommand.UserLabel) || scaffoldCommand.UserLabel == "Attack")
                        {
                            scaffoldCommand.UserLabel = scaffoldCommand.TroopType.Capitalized();
                        }

                        if (jsonCommand.TroopType != null)
                        {
                            var travelTime            = travelCalculator.CalculateTravelTime(jsonCommand.TroopType.Value, sourceVillage, targetVillage);
                            scaffoldCommand.ReturnsAt = scaffoldCommand.LandsAt + travelTime;
                        }

                        scaffoldCommand.Tx = tx;
                    }
                });

                await Profile("Save changes", () => context.SaveChangesAsync());

                //  Run upload history update in separate query to prevent creating multiple history
                //  entries
                var userUploadHistory = await EFUtil.GetOrCreateUserUploadHistory(context, CurrentUserId);

                if (jsonCommands.IsOwnCommands.Value)
                {
                    userUploadHistory.LastUploadedCommandsAt = CurrentServerTime;
                }
                else
                {
                    userUploadHistory.LastUploadedIncomingsAt = CurrentServerTime;
                }

                await context.SaveChangesAsync();

                return(Ok());
            }
            else
            {
                return(BadRequest(ModelState));
            }
        }