/// <summary>Raised after the game is launched, right before the first update tick. This happens once per game session (unrelated to loading saves). All mods are loaded and initialized at this point, so this is a good time to set up mod integrations.</summary>
        /// <param name="sender">The event sender.</param>
        /// <param name="e">The event data.</param>
        private void OnGameLaunched(object sender, GameLaunchedEventArgs e)
        {
            this.Monitor.Log("Performing init...", LogLevel.Debug);
            List <Task> tasks  = new List <Task>();
            var         start  = DateTime.Now;
            var         helper = this.Helper;

#if !DISABLE_SOUND
            // Load sounds
            tasks.Add(Task.Run(() =>
            {
                using (var monitor = new LogBuffer(this.Monitor))
                {
                    monitor.Log("Initializing sounds...", LogLevel.Trace);
                    try
                    {
                        Sound = SoundEffect.FromStream(File.OpenRead(Path.Combine(this.Helper.DirectoryPath, "assets", "Sounds", "SundropBeachNight.wav")));
                    }
                    catch (Exception err)
                    {
                        monitor.Log("Exception loading Sundrop music: " + err, LogLevel.Error);
                    }
                }
            }));
#endif
            // Load maps
            tasks.Add(Task.Run(() =>
            {
                using (var monitor = new LogBuffer(this.Monitor))
                {
                    monitor.Log("Initializing maps...", LogLevel.Trace);
                    foreach (string file in Directory.EnumerateFiles(Path.Combine(this.Helper.DirectoryPath, "assets", "Maps")))
                    {
                        string ext = Path.GetExtension(file);
                        if (ext == null || !ext.Equals(".tbin"))
                        {
                            continue;
                        }
                        string map = Path.GetFileName(file);
                        if (map == null)
                        {
                            continue;
                        }
                        try
                        {
                            monitor.Log("Map found: " + map, LogLevel.Trace);
                            var mapFile = this.Helper.Content.Load <Map>(Path.Combine("assets", "Maps", map));
                            foreach (string prop in SystemData.Layers)
                            {
                                if (mapFile.Properties.ContainsKey(prop))
                                {
                                    string rep = mapFile.Properties[prop].ToString().Replace("\n", " ").Replace(";", " ").Replace("  ", " ");
                                    while (rep.Contains("  "))
                                    {
                                        rep = rep.Replace("  ", " ");
                                    }
                                    mapFile.Properties[prop] = rep;
                                }
                            }
                            foreach (TileSheet sheet in mapFile.TileSheets)
                            {
                                if (sheet.ImageSource.EndsWith(".png"))
                                {
                                    monitor.Log(sheet.ImageSource, LogLevel.Trace);
                                    string xnb = sheet.ImageSource.Substring(0, sheet.ImageSource.Length - 4);
                                    try
                                    {
                                        Game1.content.Load <Texture2D>(xnb);
                                        sheet.ImageSource = xnb;
                                    }
                                    catch
                                    {
                                        Game1.content.Load <Texture2D>(sheet.ImageSource);
                                    }
                                }
                            }
                            var mapInst = new GameLocation(this.Helper.Content.GetActualAssetKey(Path.Combine("assets", "Maps", map)), Path.GetFileNameWithoutExtension(map));
                            this.Maps.Add(map);
                            Tourist.WarpCache.Add(Path.GetFileNameWithoutExtension(map), Tourist.GetSpawnPoints(mapInst, new HashSet <int>()
                            {
                                Tourist.TILE_WARP_DOWN, Tourist.TILE_WARP_LEFT, Tourist.TILE_WARP_RIGHT, Tourist.TILE_WARP_UP
                            }));
                            Tourist.SpawnCache.Add(Path.GetFileNameWithoutExtension(map), Tourist.GetSpawnPoints(mapInst, new HashSet <int>()
                            {
                                Tourist.TILE_ARROW_DOWN, Tourist.TILE_ARROW_LEFT, Tourist.TILE_ARROW_RIGHT, Tourist.TILE_ARROW_UP, Tourist.TILE_BROWSE, Tourist.TILE_SPAWN
                            }));
                        }
                        catch (Exception err)
                        {
                            monitor.Log("Unable to prepare `" + map + "` location, error follows\n" + err, LogLevel.Error);
                        }
                    }
                }
            }));
            tasks.Add(Task.Run(() =>
            {
                this.Monitor.Log("Mapping tourist graphics...", LogLevel.Trace);
                Tourist.LoadResources();
            }));
            tasks.Add(Task.Run(() =>
            {
                using (var monitor = new LogBuffer(this.Monitor))
                {
                    monitor.Log("Reading parking data...", LogLevel.Trace);
                    foreach (var map in helper.Content.Load <JObject>("assets/Data/ParkingSpots.json").Properties())
                    {
                        monitor.Log("Parsing parking data for the `" + map.Name + "` map, found (" + (map.Value as JArray)?.Count + ") parking spaces.", LogLevel.Trace);
                        var spotsArr = map.Value.ToObject <JArray>();
                        string name  = map.Name;
                        var spots    = new List <ParkingSpot>();
                        foreach (var spotArr in spotsArr.Cast <JArray>())
                        {
                            var values = spotArr.Children().ToArray();
                            spots.Add(new ParkingSpot(new Vector2(values[0].ToObject <int>(), values[1].ToObject <int>()), (from JValue facing in values[2].ToObject <JArray>() select(Facing) Enum.Parse(typeof(Facing), facing.ToObject <string>())).ToArray()));
                        }
                        ParkingSpots.Add(map.Name, spots.ToArray());
                    }
                }
            }));
            tasks.Add(Task.Run(() =>
            {
                using (var monitor = new LogBuffer(this.Monitor))
                {
                    monitor.Log("Reading car data...", LogLevel.Trace);
                    SundropCar.CarTypes = helper.Data.ReadJsonFile <List <CarType> >("assets/Data/Cars.json");
                    if (SundropCar.CarTypes == null)
                    {
                        monitor.Log("Unable to read Cars.json for car data, cars will not be able to spawn!", LogLevel.Error);
                        SundropCar.CarTypes = new List <CarType>();
                    }
                    else
                    {
                        Parallel.ForEach(SundropCar.CarTypes, car =>
                        {
                            car.Base    = helper.Content.GetActualAssetKey("assets/TerrainFeatures/Drivables/" + car.Base + ".png");
                            car.Recolor = helper.Content.GetActualAssetKey("assets/TerrainFeatures/Drivables/" + car.Recolor + ".png");
                            car.Decals  = car.Decals.Select(pair => new KeyValuePair <string, string>(pair.Key, helper.Content.GetActualAssetKey("assets/TerrainFeatures/Drivables/" + pair.Value + ".png"))).ToDictionary(pair => pair.Key, pair => pair.Value);
                        });
                    }
                }
            }));
            tasks.Add(Task.Run(() =>
            {
                using (var monitor = new LogBuffer(this.Monitor))
                {
                    monitor.Log("Preparing characters...", LogLevel.Trace);
                    // NPC Spawning
                    foreach (var info in this.Helper.Data.ReadJsonFile <CharacterInfo[]>("assets/Data/CharacterSpawns.json"))
                    {
                        try
                        {
                            // ReSharper disable once ObjectCreationAsStatement
                            new NPC(new AnimatedSprite(this.Helper.Content.GetActualAssetKey("assets/Characters/Sprites/" + info.Texture + ".png"), 18, 16, 32), Vector2.Zero, 2, info.Name)
                            {
                                Portrait = this.Helper.Content.Load <Texture2D>("assets/Characters/Portraits/" + info.Texture + ".png"),
                            };
                        }
                        catch (Exception err)
                        {
                            monitor.Log("Unable to prepare villager by name of `" + info.Name + "` due to a unexpected issue.\n" + err, LogLevel.Error);
                        }
                    }
                }
            }));
            tasks.Add(Task.Run(() =>
            {
                using (var monitor = new LogBuffer(this.Monitor))
                {
                    monitor.Log("Preparing cameos...", LogLevel.Trace);
                    foreach (var info in this.Helper.Data.ReadJsonFile <CharacterInfo[]>("assets/Data/CameoSpawns.json"))
                    {
                        try
                        {
                            // ReSharper disable once ObjectCreationAsStatement
                            new NPC(new AnimatedSprite(this.Helper.Content.GetActualAssetKey("assets/Characters/Cameos/" + info.Name + "Sprite.png"), 18, 16, 32), Vector2.Zero, 2, info.Name)
                            {
                                Portrait = this.Helper.Content.Load <Texture2D>("assets/Characters/Cameos/" + info.Name + "Portrait.png"),
                            };
                        }
                        catch (Exception err)
                        {
                            monitor.Log("Unable to prepare cameo character for `" + info.Name + "` due to a unexpected issue.\n" + err, LogLevel.Warn);
                        }
                    }
                }
            }));
            tasks.Add(Task.Run(() =>
            {
                this.Monitor.Log("Registering commands...", LogLevel.Trace);
                if (Config.DebugFlags.HasFlag(DebugFlags.Functions))
                {
                    helper.ConsoleCommands.Add("sundrop_debug", "For debug use only", (cmd, args) =>
                    {
                        if (args.Length == 0)
                        {
                            this.Monitor.Log("Debug command should only be used if you are asked to by a Sundrop developer!", LogLevel.Error);
                            return;
                        }
                        switch (args[0])
                        {
                        case "tourists":
                            SundropCityMod.SpawnTourists(Game1.player.currentLocation, Convert.ToInt32(args[1]));
                            this.Monitor.Log("Executed tourist spawning algorithm.", LogLevel.Alert);
                            break;

                        case "code":
                            this.TriggerDebugCode();
                            this.Monitor.Log("Debug code triggered.", LogLevel.Alert);
                            break;

                        case "warp":
                            if (args.Length == 1)
                            {
                                Game1.warpFarmer("Town", 100, 58, 1);
                            }
                            else
                            {
                                if (Game1.getLocationFromName(args[1]) == null)
                                {
                                    this.Monitor.Log("Unable to warp, target destination does not exist.", LogLevel.Error);
                                    break;
                                }
                                Game1.warpFarmer(args[1], Convert.ToSByte(args[2]) + 64, Convert.ToSByte(args[3]) + 64, false);
                            }
                            this.Monitor.Log("Warping player to target.", LogLevel.Alert);
                            break;

                        case "hotelMenu":
                            Game1.activeClickableMenu = new Hotel.Menu();
                            this.Monitor.Log("Menu for hotel triggered.", LogLevel.Alert);
                            break;

#if !DISABLE_SOUND
                        case "playSound":
                            this.Monitor.Log("Playing custom music...", LogLevel.Alert);
                            Game1.stopMusicTrack(Game1.MusicContext.Default);
                            Sound.Play(1, 1, 1);
                            break;

                        case "reportSound":
                            this.Monitor.Log("AudioEngine Wrapper: " + Game1.audioEngine.GetType().Name, LogLevel.Alert);
                            this.Monitor.Log("Wrapper Disposed: " + Game1.audioEngine.IsDisposed, LogLevel.Alert);
                            this.Monitor.Log("AudioEngine Source: " + Game1.audioEngine.Engine.GetType().Name, LogLevel.Alert);
                            this.Monitor.Log("Source Disposed: " + Game1.audioEngine.Engine.IsDisposed, LogLevel.Alert);
                            break;
#endif
                        case "map":
                            this.Monitor.Log("Name of current map: " + Game1.currentLocation.Name, LogLevel.Alert);
                            break;

                        case "position":
                            var p = Game1.player.getTileLocationPoint();
                            this.Monitor.Log($"Current tile position: {p.X}x {p.Y}y", LogLevel.Alert);
                            break;
                        }
                    });
                }
            }));
            Task.WaitAll(tasks.ToArray());
            var end  = DateTime.Now;
            var time = end.Subtract(start);
            this.Monitor.Log("Init took " + time.TotalMilliseconds + " milliseconds.", LogLevel.Debug);
        }