public static async Task RunConverterAsync(string[] args)
        {
            using var reader    = File.OpenText(string.Concat(args));
            using var converter = new AtlantisReportJsonConverter(reader);

            using JsonWriter writer = new JsonTextWriter(Console.Out);
            writer.Formatting       = Formatting.Indented;

            await converter.ConvertAsync(writer);
        }
        public async Task UploadReport([Required, FromRoute] long gameId)
        {
            using var bodyReader = new StreamReader(Request.Body);
            using var converter  = new AtlantisReportJsonConverter(bodyReader);

            using var buffer        = new MemoryStream();
            using var bufferWriter  = new StreamWriter(buffer);
            using JsonWriter writer = new JsonTextWriter(bufferWriter);
            await converter.ConvertAsync(writer);

            buffer.Seek(0, SeekOrigin.Begin);

            using var bufferReader  = new StreamReader(buffer);
            using JsonReader reader = new JsonTextReader(bufferReader);

            JObject report = await JObject.LoadAsync(reader);

            var faction         = report["faction"] as JObject;
            var date            = report["date"] as JObject;
            var engine          = report["engine"] as JObject;
            var factionStatus   = report["factionStatus"] as JArray;
            var errors          = report["errors"] as JArray;
            var events          = report["events"] as JArray;
            var skillReports    = report["skillReports"] as JArray;
            var objectReports   = report["objectReports"] as JArray;
            var itemReports     = report["itemReports"] as JArray;
            var attitudes       = report["attitudes"] as JObject;
            var unclaimedSilver = report["unclaimedSilver"];
            var regions         = report["regions"] as JArray;
            var ordersTemplate  = report["ordersTemplate"] as JObject;

            var turn = new DbTurn
            {
                GameId     = gameId,
                Year       = date.Value <int>("year"),
                Month      = MonthToNumber(date.Value <string>("month")),
                Factions   = new List <DbFaction>(),
                Regions    = new List <DbRegion>(),
                Structures = new List <DbStructure>(),
                Events     = new List <DbEvent>(),
                Units      = new List <DbUnit>()
            };

            turn.Number = (turn.Year - 1) * 12 + turn.Month;

            // ensure that there are no turn with the same number
            await RemoveTurn(gameId, turn.Number);

            db.ChangeTracker.AutoDetectChangesEnabled = false;
            var lastTurn = await db.Turns
                           .OrderByDescending(x => x.Id)
                           .Include(x => x.Factions)
                           .Include(x => x.Units)
                           .Include(x => x.Structures)
                           .Include(x => x.Regions)
                           .ThenInclude(x => x.Structures)
                           .FirstOrDefaultAsync(x => x.GameId == gameId);

            Dictionary <string, string> regionMemory = lastTurn != null
                ? lastTurn.Regions
                                                       .ToDictionary(k => k.EmpheralId, v => v.Memory)
                : new Dictionary <string, string>();

            Dictionary <int, string> unitMemory = lastTurn != null
                ? lastTurn.Units
                                                  .Where(x => x.Own)
                                                  .ToDictionary(k => k.Number, v => v.Memory)
                : new Dictionary <int, string>();

            Dictionary <string, string> structuresMemory = lastTurn != null
                ? lastTurn.Structures
                                                           .ToDictionary(k => DbStructure.GetEmpheralId(k.Region.X, k.Region.Y, k.Region.Z, k.Number, k.Type), v => v.Memory)
                : new Dictionary <string, string>();

            db.ChangeTracker.AutoDetectChangesEnabled = true;
            var game = await db.Games.FindAsync(gameId);

            turn.Memory = lastTurn?.Memory;

            await db.Turns.AddAsync(turn);

            await db.SaveChangesAsync();

            var f = new DbFaction
            {
                GameId = gameId,
                TurnId = turn.Id,
                Name   = faction.Value <string>("name"),
                Number = faction.Value <int>("number"),
                Json   = new JObject(
                    new JProperty("faction", faction),
                    new JProperty("factionStatus", factionStatus),
                    new JProperty("skillReports", skillReports),
                    new JProperty("objectReports", objectReports),
                    new JProperty("itemReports", itemReports),
                    new JProperty("attitudes", attitudes),
                    new JProperty("unclaimedSilver", unclaimedSilver)
                    ).ToString(),
            };

            if (!game.PlayerFactionNumber.HasValue)
            {
                game.PlayerFactionNumber = f.Number;
                game.EngineVersion       = engine.Value <string>("version");

                var ruleset = engine["ruleset"] as JObject;
                game.RulesetName    = ruleset.Value <string>("name");
                game.RulesetVersion = ruleset.Value <string>("version");
            }

            f.Own = f.Number == game.PlayerFactionNumber;

            f.Events = (events ?? Enumerable.Empty <JToken>())
                       .Select(x => new DbEvent {
                GameId = gameId, TurnId = turn.Id, Type = "event", Json = x.ToString()
            })
                       .Concat(
                (errors ?? Enumerable.Empty <JToken>())
                .Select(x => new DbEvent {
                GameId = gameId, TurnId = turn.Id, Type = "error", Json = x.ToString()
            })
                )
                       .ToList();

            turn.Factions.Add(f);
            turn.Events.AddRange(f.Events);

            var orders = (ordersTemplate["units"] as JArray)
                         .ToDictionary(k => k.Value <int>("unit"), v => v.Value <string>("orders"));

            foreach (JObject region in regions)
            {
                var coords = region["coords"] as JObject;

                var r = new DbRegion
                {
                    GameId        = gameId,
                    TurnId        = turn.Id,
                    UpdatedAtTurn = turn.Number,
                    X             = coords.Value <int>("x"),
                    Y             = coords.Value <int>("y"),
                    Z             = coords.TryGetValue("z", StringComparison.OrdinalIgnoreCase, out var zCoord)
                        ? zCoord.Value <int>()
                        : 1,
                    Label = coords.TryGetValue("label", StringComparison.OrdinalIgnoreCase, out var label)
                        ? label.Value <string>()
                        : "surface",
                    Province   = region.Value <string>("province"),
                    Terrain    = region.Value <string>("terrain"),
                    Structures = new List <DbStructure>(),
                    Units      = new List <DbUnit>()
                };
                r.Memory = regionMemory.TryGetValue(r.EmpheralId, out var regMem) ? regMem : null;

                var units = region["units"] as JArray;
                if (units != null)
                {
                    AddUnits(game, turn, units, r, null, unitMemory, orders);
                }

                var structures = region["structures"] as JArray;
                if (structures != null)
                {
                    AddStructures(game, turn, structures, r, structuresMemory, unitMemory, orders);
                }

                region.Remove("units");
                region.Remove("structures");
                r.Json = region.ToString();

                turn.Regions.Add(r);
            }

            if (game.PlayerFactionNumber == ordersTemplate.Value <int>("faction"))
            {
                game.Password = ordersTemplate.Value <string>("password");
            }

            // copy missing regions from previous turn
            if (lastTurn != null)
            {
                CopyInvisibleRegions(gameId, turn, lastTurn, game);
            }

            await db.SaveChangesAsync();
        }