Beispiel #1
0
            /// <summary>Generates a monster and places it on the specified map and tile.</summary>
            /// <param name="monsterType">The monster type's name and an optional dictionary of monster-specific settings.</param>
            /// <param name="location">The GameLocation where the monster should be spawned.</param>
            /// <param name="tile">The x/y coordinates of the tile where the monster should be spawned.</param>
            /// <param name="areaID">The UniqueAreaID of the related SpawnArea. Required for log messages.</param>
            /// <returns>Returns the monster's ID value, or null if the spawn process failed.</returns>
            public static int?SpawnMonster(MonsterType monsterType, GameLocation location, Vector2 tile, string areaID = "")
            {
                Monster monster = null;                                                                      //an instatiated monster, to be spawned into the world later

                Color?color = null;                                                                          //the monster's color (used by specific monster types)

                if (monsterType.Settings != null)                                                            //if settings were provided
                {
                    if (monsterType.Settings.ContainsKey("Color"))                                           //if this setting was provided
                    {
                        string[]   colorText    = ((string)monsterType.Settings["Color"]).Trim().Split(' '); //split the setting string into strings for each number
                        List <int> colorNumbers = new List <int>();
                        foreach (string text in colorText)                                                   //for each string
                        {
                            int num = Convert.ToInt32(text);                                                 //convert it to a number
                            if (num < 0)
                            {
                                num = 0;
                            }                         //minimum 0
                            else if (num > 255)
                            {
                                num = 255;
                            }                      //maximum 255
                            colorNumbers.Add(num); //add it to the list
                        }

                        //convert strings into RGBA values
                        int r = Convert.ToInt32(colorNumbers[0]);
                        int g = Convert.ToInt32(colorNumbers[1]);
                        int b = Convert.ToInt32(colorNumbers[2]);
                        int a;
                        if (colorNumbers.Count > 3) //if the setting included an "A" value
                        {
                            a = Convert.ToInt32(colorNumbers[3]);
                        }
                        else //if the setting did not include an "A" value
                        {
                            a = 255; //default to no transparency
                        }

                        color = new Color(r, g, b, a);                                                                     //set the color
                    }
                    else if (monsterType.Settings.ContainsKey("MinColor") && monsterType.Settings.ContainsKey("MaxColor")) //if color wasn't provided, but mincolor & maxcolor were
                    {
                        string[]   minColorText    = ((string)monsterType.Settings["MinColor"]).Trim().Split(' ');         //split the setting string into strings for each number
                        List <int> minColorNumbers = new List <int>();
                        foreach (string text in minColorText)                                                              //for each string
                        {
                            int num = Convert.ToInt32(text);                                                               //convert it to a number
                            if (num < 0)
                            {
                                num = 0;
                            }                         //minimum 0
                            else if (num > 255)
                            {
                                num = 255;
                            }                         //maximum 255
                            minColorNumbers.Add(num); //add it to the list
                        }

                        string[]   maxColorText    = ((string)monsterType.Settings["MaxColor"]).Trim().Split(' '); //split the setting string into strings for each number
                        List <int> maxColorNumbers = new List <int>();
                        foreach (string text in maxColorText)                                                      //for each string
                        {
                            int num = Convert.ToInt32(text);                                                       //convert it to a number
                            if (num < 0)
                            {
                                num = 0;
                            }                         //minimum 0
                            else if (num > 255)
                            {
                                num = 255;
                            }                         //maximum 255
                            maxColorNumbers.Add(num); //convert to number
                        }

                        for (int x = 0; x < minColorNumbers.Count && x < maxColorNumbers.Count; x++) //for each pair of values
                        {
                            if (minColorNumbers[x] > maxColorNumbers[x])                             //if min > max
                            {
                                //swap min and max
                                int temp = minColorNumbers[x];
                                minColorNumbers[x] = maxColorNumbers[x];
                                maxColorNumbers[x] = temp;
                            }
                        }

                        //pick random RGBA values between min and max
                        int r = RNG.Next(minColorNumbers[0], maxColorNumbers[0] + 1);
                        int g = RNG.Next(minColorNumbers[1], maxColorNumbers[1] + 1);
                        int b = RNG.Next(minColorNumbers[2], maxColorNumbers[2] + 1);
                        int a;
                        if (minColorNumbers.Count > 3 && maxColorNumbers.Count > 3) //if both settings included an "A" value
                        {
                            a = RNG.Next(minColorNumbers[3], maxColorNumbers[3] + 1);
                        }
                        else //if one/both of the settings did not include an "A" value
                        {
                            a = 255; //default to no transparency
                        }

                        color = new Color(r, g, b, a); //set the color
                    }
                }

                //set fields that affect some monster types in different ways
                bool seesPlayers     = false;                                           //whether the monster automatically "sees" players at spawn
                int  facingDirection = 2;                                               //the direction the monster should be facing at spawn
                bool rangedAttacks   = true;                                            //whether the monster is allowed to use its ranged attacks (if any)

                if (monsterType.Settings != null)                                       //if settings were provided
                {
                    if (monsterType.Settings.ContainsKey("SeesPlayersAtSpawn"))         //if this setting was provided
                    {
                        seesPlayers = (bool)monsterType.Settings["SeesPlayersAtSpawn"]; //use it
                    }

                    if (monsterType.Settings.ContainsKey("FacingDirection"))                      //if this setting was provided
                    {
                        string directionString = (string)monsterType.Settings["FacingDirection"]; //get it
                        switch (directionString.Trim().ToLower())
                        {
                        //get an integer representing the direction
                        case "up":
                            facingDirection = 0;
                            break;

                        case "right":
                            facingDirection = 1;
                            break;

                        case "down":
                            facingDirection = 2;
                            break;

                        case "left":
                            facingDirection = 3;
                            break;
                        }
                    }

                    if (monsterType.Settings.ContainsKey("RangedAttacks"))           //if this setting was provided
                    {
                        rangedAttacks = (bool)monsterType.Settings["RangedAttacks"]; //use it
                    }
                }

                //create a new monster based on the provided name & apply type-specific settings
                switch (monsterType.MonsterName.ToLower()) //avoid most casing issues by making this lower-case
                {
                case "bat":
                    monster = new BatFTM(tile, 0);
                    break;

                case "frostbat":
                case "frost bat":
                    monster = new BatFTM(tile, 40);
                    break;

                case "lavabat":
                case "lava bat":
                    monster = new BatFTM(tile, 80);
                    break;

                case "iridiumbat":
                case "iridium bat":
                    monster = new BatFTM(tile, 171);
                    break;

                case "doll":
                case "curseddoll":
                case "cursed doll":
                    monster = new BatFTM(tile, -666);
                    break;

                case "skull":
                case "hauntedskull":
                case "haunted skull":
                    monster = new BatFTM(tile, 77377);
                    break;

                case "magmasprite":
                case "magma sprite":
                    monster = new BatFTM(tile, -555);
                    break;

                case "magmasparker":
                case "magma sparker":
                    monster = new BatFTM(tile, -556);
                    break;

                case "bigslime":
                case "big slime":
                case "biggreenslime":
                case "big green slime":
                    monster = new BigSlimeFTM(tile, 0);
                    if (color.HasValue)                               //if color was provided
                    {
                        ((BigSlimeFTM)monster).c.Value = color.Value; //set its color after creation
                    }
                    if (seesPlayers)
                    {
                        monster.IsWalkingTowardPlayer = true;
                    }
                    break;

                case "bigblueslime":
                case "big blue slime":
                case "bigfrostjelly":
                case "big frost jelly":
                    monster = new BigSlimeFTM(tile, 40);
                    if (color.HasValue)                               //if color was provided
                    {
                        ((BigSlimeFTM)monster).c.Value = color.Value; //set its color after creation
                    }
                    if (seesPlayers)
                    {
                        monster.IsWalkingTowardPlayer = true;
                    }
                    break;

                case "bigredslime":
                case "big red slime":
                case "bigredsludge":
                case "big red sludge":
                    monster = new BigSlimeFTM(tile, 80);
                    if (color.HasValue)                               //if color was provided
                    {
                        ((BigSlimeFTM)monster).c.Value = color.Value; //set its color after creation
                    }
                    if (seesPlayers)
                    {
                        monster.IsWalkingTowardPlayer = true;
                    }
                    break;

                case "bigpurpleslime":
                case "big purple slime":
                case "bigpurplesludge":
                case "big purple sludge":
                    monster = new BigSlimeFTM(tile, 121);
                    if (color.HasValue)                               //if color was provided
                    {
                        ((BigSlimeFTM)monster).c.Value = color.Value; //set its color after creation
                    }
                    if (seesPlayers)                                  //if the "SeesPlayersAtSpawn" setting is true
                    {
                        monster.IsWalkingTowardPlayer = true;
                    }
                    break;

                case "bluesquid":
                case "blue squid":
                    monster = new BlueSquid(tile);
                    break;

                case "bug":
                    monster = new Bug(tile, 0);
                    break;

                case "armoredbug":
                case "armored bug":
                    monster = new Bug(tile, 121);
                    break;

                case "dino":
                case "dinomonster":
                case "dino monster":
                case "pepper":
                case "pepperrex":
                case "pepper rex":
                case "rex":
                    monster = new DinoMonster(tile);
                    if (!rangedAttacks)
                    {
                        DinoMonster dino = monster as DinoMonster;
                        dino.timeUntilNextAttack = int.MaxValue;
                        dino.nextFireTime        = int.MaxValue;
                    }
                    break;

                case "duggy":
                    monster = new DuggyFTM(tile);
                    break;

                case "magmaduggy":
                case "magma duggy":
                    monster = new DuggyFTM(tile, true);
                    break;

                case "dust":
                case "sprite":
                case "dustsprite":
                case "dust sprite":
                case "spirit":
                case "dustspirit":
                case "dust spirit":
                    monster = new DustSpiritFTM(tile);
                    break;

                case "dwarvishsentry":
                case "dwarvish sentry":
                case "dwarvish":
                case "sentry":
                    monster = new DwarvishSentry(tile);
                    for (int x = Game1.delayedActions.Count - 1; x >= 0; x--)       //check each existing DelayedAction (from last to first)
                    {
                        if (Game1.delayedActions[x].stringData == "DwarvishSentry") //if this action seems to be playing this monster's sound effect
                        {
                            Game1.delayedActions.Remove(Game1.delayedActions[x]);   //remove the action (preventing this monster's global sound effect after creation)
                            break;                                                  //skip the rest of the actions
                        }
                    }
                    break;

                case "ghost":
                    monster = new GhostFTM(tile);
                    break;

                case "carbonghost":
                case "carbon ghost":
                    monster = new GhostFTM(tile, "Carbon Ghost");
                    break;

                case "putridghost":
                case "putrid ghost":
                    monster = new GhostFTM(tile, "Putrid Ghost");
                    break;

                case "slime":
                case "greenslime":
                case "green slime":
                    monster = new GreenSlime(tile, 0);
                    if (color.HasValue)                                  //if color was also provided
                    {
                        ((GreenSlime)monster).color.Value = color.Value; //set its color after creation
                    }
                    break;

                case "blueslime":
                case "blue slime":
                case "frostjelly":
                case "frost jelly":
                    monster = new GreenSlime(tile, 40);
                    if (color.HasValue)                                  //if color was also provided
                    {
                        ((GreenSlime)monster).color.Value = color.Value; //set its color after creation
                    }
                    break;

                case "redslime":
                case "red slime":
                case "redsludge":
                case "red sludge":
                    monster = new GreenSlime(tile, 80);
                    if (color.HasValue)                                  //if color was also provided
                    {
                        ((GreenSlime)monster).color.Value = color.Value; //set its color after creation
                    }
                    break;

                case "purpleslime":
                case "purple slime":
                case "purplesludge":
                case "purple sludge":
                    monster = new GreenSlime(tile, 121);
                    if (color.HasValue)                                  //if color was also provided
                    {
                        ((GreenSlime)monster).color.Value = color.Value; //set its color after creation
                    }
                    break;

                case "tigerslime":
                case "tiger slime":
                    monster = new GreenSlime(tile, 0);                   //create any "normal" slime
                    ((GreenSlime)monster).makeTigerSlime();              //convert it into a tiger slime
                    if (color.HasValue)                                  //if color was also provided
                    {
                        ((GreenSlime)monster).color.Value = color.Value; //set its color after creation
                    }
                    break;

                case "prismaticslime":
                case "prismatic slime":
                    monster = new GreenSlime(tile, 0);                   //create any "normal" slime
                    ((GreenSlime)monster).makePrismatic();               //convert it into a prismatic slime
                    if (color.HasValue)                                  //if color was also provided
                    {
                        ((GreenSlime)monster).color.Value = color.Value; //set its color after creation
                    }
                    break;

                case "grub":
                case "cavegrub":
                case "cave grub":
                    monster = new GrubFTM(tile, false);
                    break;

                case "fly":
                case "cavefly":
                case "cave fly":
                    monster = new FlyFTM(tile, false);
                    break;

                case "mutantgrub":
                case "mutant grub":
                    monster = new GrubFTM(tile, true);
                    break;

                case "mutantfly":
                case "mutant fly":
                    monster = new FlyFTM(tile, true);
                    break;

                case "metalhead":
                case "metal head":
                    monster = new MetalHead(tile, 0);
                    if (color.HasValue)                             //if color was provided
                    {
                        ((MetalHead)monster).c.Value = color.Value; //set its color after creation
                    }
                    break;

                case "hothead":
                case "hot head":
                    monster = new HotHead(tile);
                    if (color.HasValue)                           //if color was provided
                    {
                        ((HotHead)monster).c.Value = color.Value; //set its color after creation
                    }
                    break;

                case "lavalurk":
                case "lava lurk":
                    monster = new LavaLurkFTM(tile, rangedAttacks);
                    break;

                case "leaper":
                    monster = new Leaper(tile);
                    break;

                case "mummy":
                    monster = new MummyFTM(tile);
                    break;

                case "rockcrab":
                case "rock crab":
                    monster = new RockCrab(tile);
                    break;

                case "lavacrab":
                case "lava crab":
                    monster = new LavaCrab(tile);
                    break;

                case "iridiumcrab":
                case "iridium crab":
                    monster = new RockCrab(tile, "Iridium Crab");
                    break;

                case "falsemagmacap":
                case "false magma cap":
                case "magmacap":
                case "magma cap":
                    monster            = new RockCrab(tile, "False Magma Cap");
                    monster.HideShadow = true;     //hide shadow, making them look more like "real" magma caps
                    break;

                case "stickbug":
                case "stick bug":
                    monster = new RockCrab(tile);
                    (monster as RockCrab).makeStickBug();
                    break;

                case "rockgolem":
                case "rock golem":
                case "stonegolem":
                case "stone golem":
                    monster = new RockGolemFTM(tile);
                    break;

                case "wildernessgolem":
                case "wilderness golem":
                    monster = new RockGolemFTM(tile, Game1.player.CombatLevel);
                    break;

                case "serpent":
                    monster = new SerpentFTM(tile);
                    break;

                case "royalserpent":
                case "royal serpent":
                    monster = new SerpentFTM(tile, "Royal Serpent");
                    break;

                case "brute":
                case "shadowbrute":
                case "shadow brute":
                    monster = new ShadowBrute(tile);
                    break;

                case "shaman":
                case "shadowshaman":
                case "shadow shaman":
                    monster = new ShadowShaman(tile);
                    if (!rangedAttacks)
                    {
                        Helper.Reflection.GetField <int>(monster, "coolDown", false)?.SetValue(int.MaxValue);    //set spell cooldown to max
                    }
                    break;

                case "sniper":
                case "shadowsniper":
                case "shadow sniper":
                    monster = new Shooter(tile);
                    if (!rangedAttacks)
                    {
                        (monster as Shooter).nextShot = float.MaxValue;     //set shot cooldown to max
                    }
                    break;

                case "skeleton":
                    monster = new SkeletonFTM(tile, false, rangedAttacks);
                    if (seesPlayers)
                    {
                        Helper.Reflection.GetField <bool>(monster, "spottedPlayer", false)?.SetValue(true);    //set "spotted player" field to true
                        monster.IsWalkingTowardPlayer = true;
                    }
                    break;

                case "skeletonmage":
                case "skeleton mage":
                    monster = new SkeletonFTM(tile, true, rangedAttacks);
                    if (seesPlayers)
                    {
                        Helper.Reflection.GetField <bool>(monster, "spottedPlayer", false)?.SetValue(true);    //set "spotted player" field to true
                        monster.IsWalkingTowardPlayer = true;
                    }
                    break;

                case "spiker":
                    monster = new Spiker(tile, facingDirection);
                    break;

                case "squidkid":
                case "squid kid":
                    monster = new SquidKidFTM(tile);
                    if (!rangedAttacks)
                    {
                        Helper.Reflection.GetField <int>(monster, "lastFireball", false)?.SetValue(int.MaxValue);    //set fireball cooldown to max
                    }
                    break;

                default:                                                                           //if the name doesn't match any directly known monster types
                    //check MTF monster types
                    if (MonstersTheFrameworkAPI.IsKnownMonsterType(monsterType.MonsterName, true)) //if this is a known (and previously validated) monster type from MTF
                    {
                        monster = MonstersTheFrameworkAPI.CreateMonster(monsterType.MonsterName);  //create it through the MTF interface
                        break;
                    }
                    //handle the name as a custom Type
                    Type externalType = GetTypeFromName(monsterType.MonsterName, typeof(Monster)); //find a monster subclass with a matching name
                    monster = (Monster)Activator.CreateInstance(externalType, tile);               //create a monster with the Vector2 constructor
                    break;
                }

                if (monster == null)
                {
                    Monitor.Log($"The monster to be spawned (\"{monsterType.MonsterName}\") doesn't match any known monster types. Make sure that name isn't misspelled in your config file.", LogLevel.Info);
                    return(null);
                }

                int?ID = MonsterTracker.AddMonster(monster);  //generate an ID for this monster

                if (!ID.HasValue)
                {
                    Monitor.Log("A new monster ID could not be generated. This is may be due to coding issue; please report it to this mod's developer. This monster won't be spawned.", LogLevel.Warn);
                    return(null);
                }
                monster.id = ID.Value;                                       //assign the ID to this monster

                monster.MaxHealth = monster.Health;                          //some monster types set Health on creation and expect MaxHealth to be updated like this

                ApplyMonsterSettings(monster, monsterType.Settings, areaID); //adjust the monster based on any other provided optional settings

                //spawn the completed monster at the target location
                Monitor.VerboseLog($"Spawning monster. Type: {monsterType.MonsterName}. Location: {tile.X},{tile.Y} ({location.Name}).");
                monster.currentLocation = location;
                monster.setTileLocation(tile);
                location.addCharacter(monster);
                return(monster.id);
            }
            /// <summary>Removes any invalid monster types and/or settings from a list.</summary>
            /// <param name="monsterTypes">A list of monster type data.</param>
            /// <param name="areaID">The UniqueAreaID of the related SpawnArea. Required for log messages.</param>
            /// <returns>A new list of MonsterTypes with any invalid monster types and/or settings removed.</returns>
            public static List <MonsterType> ValidateMonsterTypes(List <MonsterType> monsterTypes, string areaID = "")
            {
                if (monsterTypes == null || monsterTypes.Count <= 0) //if the provided list is null or empty
                {
                    return(new List <MonsterType>());                //return an empty list
                }

                List <MonsterType> validTypes = Clone(monsterTypes); //create a new copy of the list, to be validated and returned

                for (int x = validTypes.Count - 1; x >= 0; x--)      //for each monster type in the new list (iterating backward to allow safe removal)
                {
                    //validate monster names
                    bool validName = false;

                    //NOTE: these switch cases are copied from SpawnMonster.cs; update them manually when new monsters are added
                    switch (validTypes[x].MonsterName.ToLower()) //avoid any casing issues by making this lower-case
                    {
                    case "bat":
                    case "frostbat":
                    case "frost bat":
                    case "lavabat":
                    case "lava bat":
                    case "iridiumbat":
                    case "iridium bat":
                    case "doll":
                    case "curseddoll":
                    case "cursed doll":
                    case "skull":
                    case "hauntedskull":
                    case "haunted skull":
                    case "magmasprite":
                    case "magma sprite":
                    case "magmasparker":
                    case "magma sparker":
                    case "bigslime":
                    case "big slime":
                    case "biggreenslime":
                    case "big green slime":
                    case "bigblueslime":
                    case "big blue slime":
                    case "bigfrostjelly":
                    case "big frost jelly":
                    case "bigredslime":
                    case "big red slime":
                    case "bigredsludge":
                    case "big red sludge":
                    case "bigpurpleslime":
                    case "big purple slime":
                    case "bigpurplesludge":
                    case "big purple sludge":
                    case "bluesquid":
                    case "blue squid":
                    case "bug":
                    case "armoredbug":
                    case "armored bug":
                    case "dino":
                    case "dinomonster":
                    case "dino monster":
                    case "pepper":
                    case "pepperrex":
                    case "pepper rex":
                    case "rex":
                    case "duggy":
                    case "magmaduggy":
                    case "magma duggy":
                    case "dust":
                    case "sprite":
                    case "dustsprite":
                    case "dust sprite":
                    case "spirit":
                    case "dustspirit":
                    case "dust spirit":
                    case "dwarvishsentry":
                    case "dwarvish sentry":
                    case "dwarvish":
                    case "sentry":
                    case "ghost":
                    case "carbonghost":
                    case "carbon ghost":
                    case "putridghost":
                    case "putrid ghost":
                    case "slime":
                    case "greenslime":
                    case "green slime":
                    case "blueslime":
                    case "blue slime":
                    case "frostjelly":
                    case "frost jelly":
                    case "redslime":
                    case "red slime":
                    case "redsludge":
                    case "red sludge":
                    case "purpleslime":
                    case "purple slime":
                    case "purplesludge":
                    case "purple sludge":
                    case "tigerslime":
                    case "tiger slime":
                    case "prismaticslime":
                    case "prismatic slime":
                    case "grub":
                    case "cavegrub":
                    case "cave grub":
                    case "fly":
                    case "cavefly":
                    case "cave fly":
                    case "mutantgrub":
                    case "mutant grub":
                    case "mutantfly":
                    case "mutant fly":
                    case "metalhead":
                    case "metal head":
                    case "hothead":
                    case "hot head":
                    case "lavalurk":
                    case "lava lurk":
                    case "leaper":
                    case "mummy":
                    case "rockcrab":
                    case "rock crab":
                    case "lavacrab":
                    case "lava crab":
                    case "iridiumcrab":
                    case "iridium crab":
                    case "falsemagmacap":
                    case "false magma cap":
                    case "stickbug":
                    case "stick bug":
                    case "magmacap":
                    case "magma cap":
                    case "rockgolem":
                    case "rock golem":
                    case "stonegolem":
                    case "stone golem":
                    case "wildernessgolem":
                    case "wilderness golem":
                    case "serpent":
                    case "royalserpent":
                    case "royal serpent":
                    case "brute":
                    case "shadowbrute":
                    case "shadow brute":
                    case "shaman":
                    case "shadowshaman":
                    case "shadow shaman":
                    case "sniper":
                    case "shadowsniper":
                    case "shadow sniper":
                    case "skeleton":
                    case "skeletonmage":
                    case "skeleton mage":
                    case "spiker":
                    case "squidkid":
                    case "squid kid":
                        validName = true;     //the name is valid
                        break;

                    default:                                                                              //if the name doesn't match any directly known monster types
                        //check for MTF monster types
                        if (MonstersTheFrameworkAPI.IsKnownMonsterType(validTypes[x].MonsterName, false)) //if this is a known monster type from MTF
                        {
                            validName = true;                                                             //the name is valid
                            break;
                        }
                        //check for custom monster classes
                        Type externalType = GetTypeFromName(validTypes[x].MonsterName, typeof(Monster)); //find a monster subclass with a matching name
                        if (externalType != null)                                                        //if a matching type was found
                        {
                            if (externalType.GetConstructor(new[] { typeof(Vector2) }) != null)          //if this type has a constructor that takes a Vector2
                            {
                                validName = true;                                                        //the name is valid
                            }

                            /* NOTE: Accepting monsters' default constructors would be dangerous and is not recommended.
                             * Many monsters' defaults create them without filling game-critical fields, and this code can't reasonably account for them.
                             * The game will often freeze or crash if they are used here.
                             */
                        }
                        break;
                    }

                    if (!validName) //if the name is invalid
                    {
                        Monitor.Log($"A listed monster (\"{validTypes[x].MonsterName}\") doesn't match any known monster types. Make sure that name isn't misspelled in your config file.", LogLevel.Info);
                        Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                        validTypes.RemoveAt(x); //remove this type from the valid list
                        continue;               //skip to the next monster type
                    }

                    if (validTypes[x].Settings == null)                             //if no settings were provided at all
                    {
                        validTypes[x].Settings = new Dictionary <string, object>(); //create a blank list of settings
                    }

                    //validate HP
                    if (validTypes[x].Settings.ContainsKey("HP"))
                    {
                        if (validTypes[x].Settings["HP"] is long) //if this is a readable integer
                        {
                            int HP = Convert.ToInt32(validTypes[x].Settings["HP"]);
                            if (HP < 1) //if the setting is too low
                            {
                                Monitor.Log($"The \"HP\" setting for monster type \"{validTypes[x].MonsterName}\" is {HP}. Setting it to 1.", LogLevel.Trace);
                                validTypes[x].Settings["HP"] = (long)1; //set the validated setting to 1
                            }
                        }
                        else //if this isn't a readable integer
                        {
                            Monitor.Log($"The \"HP\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's an integer.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("HP"); //remove the setting
                        }
                    }

                    //validate damage
                    if (validTypes[x].Settings.ContainsKey("Damage"))
                    {
                        if (validTypes[x].Settings["Damage"] is long) //if this is a readable integer
                        {
                            int damage = Convert.ToInt32(validTypes[x].Settings["Damage"]);
                            if (damage < 0) //if the setting is too low
                            {
                                Monitor.Log($"The \"Damage\" setting for monster type \"{validTypes[x].MonsterName}\" is {damage}. Setting it to 0.", LogLevel.Trace);
                                validTypes[x].Settings["Damage"] = (long)0; //set the validated setting to 0
                            }
                        }
                        else //if this isn't a readable integer
                        {
                            Monitor.Log($"The \"Damage\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's an integer.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("Damage"); //remove the setting
                        }
                    }

                    //validate defense
                    if (validTypes[x].Settings.ContainsKey("Defense"))
                    {
                        if (validTypes[x].Settings["Defense"] is long) //if this is a readable integer
                        {
                            int defense = Convert.ToInt32(validTypes[x].Settings["Defense"]);
                            if (defense < 0) //if the setting is too low
                            {
                                Monitor.Log($"The \"Defense\" setting for monster type \"{validTypes[x].MonsterName}\" is {defense}. Setting it to 0.", LogLevel.Trace);
                                validTypes[x].Settings["Defense"] = (long)0; //set the validated setting to 1
                            }
                        }
                        else //if this isn't a readable integer
                        {
                            Monitor.Log($"The \"Defense\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's an integer.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("Defense"); //remove the setting
                        }
                    }

                    //validate dodge chance
                    if (validTypes[x].Settings.ContainsKey("DodgeChance"))
                    {
                        if (validTypes[x].Settings["DodgeChance"] is long) //if this is a readable integer
                        {
                            int dodge = Convert.ToInt32(validTypes[x].Settings["DodgeChance"]);
                            if (dodge < 1) //if the setting is too low
                            {
                                Monitor.Log($"The \"DodgeChance\" setting for monster type \"{validTypes[x].MonsterName}\" is {dodge}. Setting it to 1.", LogLevel.Trace);
                                validTypes[x].Settings["DodgeChance"] = (long)1; //set the validated setting to 1
                            }
                        }
                        else //if this isn't a readable integer
                        {
                            Monitor.Log($"The \"DodgeChance\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's an integer.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("DodgeChance"); //remove the setting
                        }
                    }

                    //validate experience points
                    if (validTypes[x].Settings.ContainsKey("EXP"))
                    {
                        if (validTypes[x].Settings["EXP"] is long) //if this is a readable integer
                        {
                            int exp = Convert.ToInt32(validTypes[x].Settings["EXP"]);
                            if (exp < 0) //if the setting is too low
                            {
                                Monitor.Log($"The \"EXP\" setting for monster type \"{validTypes[x].MonsterName}\" is {exp}. Setting it to 0.", LogLevel.Trace);
                                validTypes[x].Settings["EXP"] = (long)0; //set the validated setting to 0
                            }
                        }
                        else //if this isn't a readable integer
                        {
                            Monitor.Log($"The \"EXP\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's an integer.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("EXP"); //remove the setting
                        }
                    }

                    //validate the related skill setting
                    if (validTypes[x].Settings.ContainsKey("RelatedSkill"))
                    {
                        if (validTypes[x].Settings["RelatedSkill"] is string)                                        //if this is a readable string
                        {
                            string relatedSkill = ((string)validTypes[x].Settings["RelatedSkill"]).Trim().ToLower(); //parse the provided skill, trim whitespace, and lower case
                            bool   isSkill      = false;

                            foreach (string skill in Enum.GetNames(typeof(Skills)))          //for each known in-game skill
                            {
                                if (relatedSkill.Trim().ToLower() == skill.Trim().ToLower()) //if the provided skill name matches this known skill
                                {
                                    isSkill = true;                                          //the provided skill is valid
                                }
                            }

                            if (!isSkill) //if this isn't a known skill
                            {
                                Monitor.Log($"The \"RelatedSkill\" setting for monster type \"{validTypes[x].MonsterName}\" doesn't seem to be a known skill. Please make sure it's spelled correctly.", LogLevel.Info);
                                Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                                validTypes[x].Settings.Remove("RelatedSkill"); //remove the setting
                            }
                        }
                        else //if this isn't a readable string
                        {
                            Monitor.Log($"The \"RelatedSkill\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's a valid string (text inside quotation marks).", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("RelatedSkill"); //remove the setting
                        }
                    }

                    //validate minimum skill requirement
                    if (validTypes[x].Settings.ContainsKey("MinimumSkillLevel"))
                    {
                        if (validTypes[x].Settings["MinimumSkillLevel"] is long)    //if this is a readable integer
                        {
                            if (validTypes[x].Settings.ContainsKey("RelatedSkill")) //if a RelatedSkill has been provided
                            {
                                int required          = Convert.ToInt32(validTypes[x].Settings["MinimumSkillLevel"]);
                                int highestSkillLevel = 0;                                                                      //highest skill level among all existing farmers (not just the host)
                                Enum.TryParse((string)validTypes[x].Settings["RelatedSkill"], true, out Skills skill);          //parse the RelatedSkill setting into an enum (note: the setting should be validated earlier in this method)

                                foreach (Farmer farmer in Game1.getAllFarmers())                                                //for each farmer
                                {
                                    highestSkillLevel = Math.Max(highestSkillLevel, farmer.getEffectiveSkillLevel((int)skill)); //record the farmer's skill level if it's higher than before
                                }

                                if (required > highestSkillLevel) //if the skill requirement is higher than the farmers' highest skill
                                {
                                    Monitor.VerboseLog($"Skipping monster type \"{validTypes[x].MonsterName}\" in spawn area \"{areaID}\" due to minimum skill level.");
                                    validTypes.RemoveAt(x); //remove this type from the valid list
                                    continue;               //skip to the next monster type
                                }
                            }
                            else //if a RelatedSkill was not provided
                            {
                                Monitor.Log($"Monster type \"{validTypes[x].MonsterName}\" has a valid setting for \"MinimumSkillLevel\" but not \"RelatedSkill\". The requirement will be skipped.", LogLevel.Info);
                                Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                                validTypes[x].Settings.Remove("MinimumSkillLevel"); //remove the setting
                            }
                        }
                        else //if this isn't a readable integer
                        {
                            Monitor.Log($"The \"MinimumSkillLevel\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's an integer.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("MinimumSkillLevel"); //remove the setting
                        }
                    }

                    //validate maximum skill requirement
                    if (validTypes[x].Settings.ContainsKey("MaximumSkillLevel"))
                    {
                        if (validTypes[x].Settings["MaximumSkillLevel"] is long)    //if this is a readable integer
                        {
                            if (validTypes[x].Settings.ContainsKey("RelatedSkill")) //if a RelatedSkill has been provided
                            {
                                int required          = Convert.ToInt32(validTypes[x].Settings["MaximumSkillLevel"]);
                                int highestSkillLevel = 0;                                                                      //highest skill level among all existing farmers (not just the host)
                                Enum.TryParse((string)validTypes[x].Settings["RelatedSkill"], true, out Skills skill);          //parse the RelatedSkill setting into an enum (note: the setting should be validated earlier in this method)

                                foreach (Farmer farmer in Game1.getAllFarmers())                                                //for each farmer
                                {
                                    highestSkillLevel = Math.Max(highestSkillLevel, farmer.getEffectiveSkillLevel((int)skill)); //record the farmer's skill level if it's higher than before
                                }

                                if (required < highestSkillLevel) //if the skill requirement is lower than the farmers' highest skill
                                {
                                    Monitor.VerboseLog($"Skipping monster type \"{validTypes[x].MonsterName}\" in spawn area \"{areaID}\" due to maximum skill level.");
                                    validTypes.RemoveAt(x); //remove this type from the valid list
                                    continue;               //skip to the next monster type
                                }
                            }
                            else //if a RelatedSkill was not provided
                            {
                                Monitor.Log($"Monster type \"{validTypes[x].MonsterName}\" has a valid setting for \"MaximumSkillLevel\" but not \"RelatedSkill\". The requirement will be skipped.", LogLevel.Info);
                                Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                                validTypes[x].Settings.Remove("MaximumSkillLevel"); //remove the setting
                            }
                        }
                        else //if this isn't a readable integer
                        {
                            Monitor.Log($"The \"MaximumSkillLevel\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's an integer.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("MaximumSkillLevel"); //remove the setting
                        }
                    }

                    //validate HP multiplier
                    if (validTypes[x].Settings.ContainsKey("PercentExtraHPPerSkillLevel"))
                    {
                        if (!(validTypes[x].Settings["PercentExtraHPPerSkillLevel"] is long)) //if this isn't a readable integer
                        {
                            Monitor.Log($"The \"PercentExtraHPPerSkillLevel\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's an integer.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("PercentExtraHPPerSkillLevel"); //remove the setting
                        }
                    }

                    //validate damage multiplier
                    if (validTypes[x].Settings.ContainsKey("PercentExtraDamagePerSkillLevel"))
                    {
                        if (!(validTypes[x].Settings["PercentExtraDamagePerSkillLevel"] is long)) //if this isn't a readable integer
                        {
                            Monitor.Log($"The \"PercentExtraDamagePerSkillLevel\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's an integer.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("PercentExtraDamagePerSkillLevel"); //remove the setting
                        }
                    }

                    //validate defense multiplier
                    if (validTypes[x].Settings.ContainsKey("PercentExtraDefensePerSkillLevel"))
                    {
                        if (!(validTypes[x].Settings["PercentExtraDefensePerSkillLevel"] is long)) //if this isn't a readable integer
                        {
                            Monitor.Log($"The \"PercentExtraDefensePerSkillLevel\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's an integer.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("PercentExtraDefensePerSkillLevel"); //remove the setting
                        }
                    }

                    //validate dodge chance multiplier
                    if (validTypes[x].Settings.ContainsKey("PercentExtraDodgeChancePerSkillLevel"))
                    {
                        if (!(validTypes[x].Settings["PercentExtraDodgeChancePerSkillLevel"] is long)) //if this isn't a readable integer
                        {
                            Monitor.Log($"The \"PercentExtraDodgeChancePerSkillLevel\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's an integer.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("PercentExtraDodgeChancePerSkillLevel"); //remove the setting
                        }
                    }

                    //validate experience multiplier
                    if (validTypes[x].Settings.ContainsKey("PercentExtraEXPPerSkillLevel"))
                    {
                        if (!(validTypes[x].Settings["PercentExtraEXPPerSkillLevel"] is long)) //if this isn't a readable integer
                        {
                            Monitor.Log($"The \"PercentExtraEXPPerSkillLevel\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's an integer.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("PercentExtraEXPPerSkillLevel"); //remove the setting
                        }
                    }

                    //validate loot and parse the provided objects into IDs
                    if (validTypes[x].Settings.ContainsKey("Loot"))
                    {
                        List <object> rawList = null;

                        try
                        {
                            rawList = ((JArray)validTypes[x].Settings["Loot"]).ToObject <List <object> >(); //cast this list to catch formatting/coding errors
                        }
                        catch (Exception)
                        {
                            Monitor.Log($"The \"Loot\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's a correctly formatted list.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("Loot"); //remove the setting
                        }

                        if (validTypes[x].Settings.ContainsKey("Loot"))                    //if no exception happened
                        {
                            if (rawList == null)                                           //if a null list was provided
                            {
                                validTypes[x].Settings["Loot"] = new List <SavedObject>(); //use an empty list
                            }
                            else //if an actual list was provided
                            {
                                List <SavedObject> lootList = ParseSavedObjectsFromItemList(rawList, areaID); //parse the object list into a SavedObject list

                                foreach (SavedObject loot in lootList)
                                {
                                    //convert any "object" categories to "item" for the loot list
                                    string category = loot.ConfigItem?.Category?.ToLower();
                                    if (category == "object" || category == "objects")
                                    {
                                        loot.ConfigItem.Category = "item";
                                    }
                                }

                                validTypes[x].Settings["Loot"] = lootList;
                            }
                        }
                    }

                    //validate persistent HP
                    if (validTypes[x].Settings.ContainsKey("PersistentHP"))
                    {
                        if (!(validTypes[x].Settings["PersistentHP"] is bool)) //if this is NOT a readable boolean
                        {
                            Monitor.Log($"The \"PersistentHP\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's true or false (without quotation marks).", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("PersistentHP"); //remove the setting
                        }
                    }

                    //validate current HP
                    if (validTypes[x].Settings.ContainsKey("CurrentHP"))
                    {
                        if (validTypes[x].Settings["CurrentHP"] is long) //if this is a readable integer
                        {
                            int currentHP = Convert.ToInt32(validTypes[x].Settings["CurrentHP"]);
                            if (currentHP < 1) //if the current HP setting is too low
                            {
                                Monitor.Log($"The \"CurrentHP\" setting for monster type \"{validTypes[x].MonsterName}\" is {currentHP}. Setting it to 1.", LogLevel.Trace);
                                monsterTypes[x].Settings["CurrentHP"] = (long)1; //set the original provided setting to 1
                                validTypes[x].Settings["CurrentHP"]   = (long)1; //set the validated setting to 1
                            }
                        }
                        else //if this isn't a readable integer
                        {
                            Monitor.Log($"The \"CurrentHP\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's an integer.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("CurrentHP"); //remove the setting
                        }
                    }

                    //validate seeing players at spawn
                    if (validTypes[x].Settings.ContainsKey("SeesPlayersAtSpawn"))
                    {
                        if (!(validTypes[x].Settings["SeesPlayersAtSpawn"] is bool)) //if this is NOT a readable boolean
                        {
                            Monitor.Log($"The \"SeesPlayersAtSpawn\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's true or false (without quotation marks).", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("SeesPlayersAtSpawn"); //remove the setting
                        }
                    }

                    //validate color
                    if (validTypes[x].Settings.ContainsKey("Color")) //if color was provided
                    {
                        //try a trimmed copy of the color application code
                        try
                        {
                            string[]   colorText    = ((string)validTypes[x].Settings["Color"]).Trim().Split(' '); //split the color string into strings for each number
                            List <int> colorNumbers = new List <int>();
                            foreach (string text in colorText)                                                     //for each string
                            {
                                int num = Convert.ToInt32(text);                                                   //convert it to a number
                                if (num < 0)
                                {
                                    num = 0;
                                }                         //minimum 0
                                else if (num > 255)
                                {
                                    num = 255;
                                }                      //maximum 255
                                colorNumbers.Add(num); //add it to the list
                            }

                            //convert strings into RGBA values
                            int r = Convert.ToInt32(colorNumbers[0]);
                            int g = Convert.ToInt32(colorNumbers[1]);
                            int b = Convert.ToInt32(colorNumbers[2]);
                            int a;
                            if (colorNumbers.Count > 3) //if the setting included an "A" value
                            {
                                a = Convert.ToInt32(colorNumbers[3]);
                            }
                        }
                        catch (Exception)
                        {
                            Monitor.Log($"The \"Color\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it follows the correct format, e.g. \"255 255 255\" or \"255 255 255 255\".", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("Color"); //remove the setting
                        }
                    }
                    else if (validTypes[x].Settings.ContainsKey("MinColor") && validTypes[x].Settings.ContainsKey("MaxColor")) //if color wasn't provided, but mincolor & maxcolor were
                    {
                        //try a trimmed copy of the min/max color application code
                        try
                        {
                            string[]   minColorText    = ((string)validTypes[x].Settings["MinColor"]).Trim().Split(' '); //split the setting string into strings for each number
                            List <int> minColorNumbers = new List <int>();
                            foreach (string text in minColorText)                                                        //for each string
                            {
                                int num = Convert.ToInt32(text);                                                         //convert it to a number
                                if (num < 0)
                                {
                                    num = 0;
                                }                         //minimum 0
                                else if (num > 255)
                                {
                                    num = 255;
                                }                         //maximum 255
                                minColorNumbers.Add(num); //add it to the list
                            }

                            string[]   maxColorText    = ((string)validTypes[x].Settings["MaxColor"]).Trim().Split(' '); //split the setting string into strings for each number
                            List <int> maxColorNumbers = new List <int>();
                            foreach (string text in maxColorText)                                                        //for each string
                            {
                                int num = Convert.ToInt32(text);                                                         //convert it to a number
                                if (num < 0)
                                {
                                    num = 0;
                                }                         //minimum 0
                                else if (num > 255)
                                {
                                    num = 255;
                                }                         //maximum 255
                                maxColorNumbers.Add(num); //convert to number
                            }
                        }
                        catch (Exception)
                        {
                            Monitor.Log($"The \"MinColor\" and/or \"MaxColor\" settings for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure they follow the correct format, e.g. \"255 255 255\".", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            //remove the settings
                            validTypes[x].Settings.Remove("MinColor");
                            validTypes[x].Settings.Remove("MaxColor");
                        }
                    }

                    //validate sprite
                    if (validTypes[x].Settings.ContainsKey("Sprite"))
                    {
                        if (validTypes[x].Settings["Sprite"] is string spriteText) //if this is a readable string
                        {
                            try
                            {
                                AnimatedSprite sprite = new AnimatedSprite(spriteText);
                            }
                            catch (Exception)
                            {
                                Monitor.Log($"The \"Sprite\" setting for monster type \"{validTypes[x].MonsterName}\" failed to load. Please make sure the setting is spelled correctly.", LogLevel.Info);
                                Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                                validTypes[x].Settings.Remove("Sprite"); //remove the setting
                            }
                        }
                        else //if this is NOT a readable string
                        {
                            Monitor.Log($"The \"Sprite\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's a valid string (text inside quotation marks).", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("Sprite"); //remove the setting
                        }
                    }

                    //validate spawn weight
                    if (validTypes[x].Settings.ContainsKey("SpawnWeight"))
                    {
                        if (validTypes[x].Settings["SpawnWeight"] is long) //if this is a readable integer
                        {
                            int weight = Convert.ToInt32(validTypes[x].Settings["SpawnWeight"]);
                            if (weight < 1) //if the setting is too low
                            {
                                Monitor.Log($"The \"SpawnWeight\" setting for monster type \"{validTypes[x].MonsterName}\" is {weight} and will be ignored. Please use a number above 0.", LogLevel.Trace);
                                validTypes[x].Settings["SpawnWeight"] = (long)1; //set to 1
                            }
                        }
                        else //if this isn't a readable integer
                        {
                            Monitor.Log($"The \"SpawnWeight\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's an integer.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("SpawnWeight"); //remove the setting
                        }
                    }

                    //validate facing direction
                    if (validTypes[x].Settings.ContainsKey("FacingDirection"))
                    {
                        if (validTypes[x].Settings["FacingDirection"] is string direction) //if this is a string
                        {
                            switch (direction.Trim().ToLower())
                            {
                            case "up":
                            case "right":
                            case "down":
                            case "left":
                                break;

                            default:
                                Monitor.Log($"The \"FacingDirection\" setting for monster type \"{validTypes[x].MonsterName}\" was not recognized and will be ignored: \"{direction}\".", LogLevel.Info);
                                Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);
                                validTypes[x].Settings.Remove("FacingDirection");     //remove the setting
                                break;
                            }
                        }
                        else //if this is NOT a string
                        {
                            Monitor.Log($"The \"FacingDirection\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's a string.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("FacingDirection"); //remove the setting
                        }
                    }

                    //validate segments
                    if (validTypes[x].Settings.ContainsKey("Segments"))
                    {
                        if (validTypes[x].Settings["Segments"] is long) //if this is a readable integer
                        {
                            //do nothing; minimum values will vary between monster types
                        }
                        else //if this isn't a readable integer
                        {
                            Monitor.Log($"The \"Segments\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's an integer.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("Segments"); //remove the setting
                        }
                    }

                    //validate sight range
                    if (validTypes[x].Settings.ContainsKey("SightRange"))
                    {
                        if (validTypes[x].Settings["SightRange"] is long) //if this is a readable integer
                        {
                            //do nothing
                        }
                        else //if this isn't a readable integer
                        {
                            Monitor.Log($"The \"SightRange\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's an integer.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("SightRange"); //remove the setting
                        }
                    }

                    //validate ranged attacks
                    if (validTypes[x].Settings.ContainsKey("RangedAttacks"))
                    {
                        if (!(validTypes[x].Settings["RangedAttacks"] is bool)) //if this is NOT a readable boolean
                        {
                            Monitor.Log($"The \"RangedAttacks\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's true or false (without quotation marks).", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("RangedAttacks"); //remove the setting
                        }
                    }

                    //validate extra loot
                    if (validTypes[x].Settings.ContainsKey("ExtraLoot"))
                    {
                        if (!(validTypes[x].Settings["ExtraLoot"] is bool)) //if this is NOT a readable boolean
                        {
                            Monitor.Log($"The \"ExtraLoot\" setting for monster type \"{validTypes[x].MonsterName}\" couldn't be parsed. Please make sure it's true or false (without quotation marks).", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: {areaID}", LogLevel.Info);

                            validTypes[x].Settings.Remove("ExtraLoot"); //remove the setting
                        }
                    }
                }

                return(validTypes);
            }