Ejemplo n.º 1
0
        /// <summary>Produces a list of x/y coordinates for valid, open tiles for object spawning at a location (based on tile index, e.g. tiles using a specific dirt texture).</summary>
        /// <param name="area">The SpawnArea describing the current area and its settings.</param>
        /// <param name="tileIndices">A list of integers representing spritesheet tile indices. Tiles with any matching index will be checked for object spawning.</param>
        /// <param name="isLarge">True if the objects to be spawned are 2x2 tiles in size, otherwise false (1 tile).</param>
        /// <returns>A list of Vector2, each representing a valid, open tile for object spawning at the given location.</returns>
        public static List <Vector2> GetTilesByIndex(SpawnArea area, int[] tileIndices, bool isLarge)
        {
            GameLocation   loc        = Game1.getLocationFromName(area.MapName); //variable for the current location being worked on
            List <Vector2> validTiles = new List <Vector2>();                    //will contain x,y coordinates for tiles that are open & valid for new object placement

            //the following loops should populate a list of valid, open tiles for spawning
            int currentTileIndex;

            for (int y = 0; y < (loc.Map.DisplayHeight / Game1.tileSize); y++)
            {
                for (int x = 0; x < (loc.Map.DisplayWidth / Game1.tileSize); x++) //loops for each tile on the map, from the top left (x,y == 0,0) to bottom right, moving horizontally first
                {
                    Vector2 tile = new Vector2(x, y);
                    currentTileIndex = loc.getTileIndexAt(x, y, "Back"); //get the tile index of the current tile
                    foreach (int index in tileIndices)
                    {
                        if (currentTileIndex == index)            //if the current tile matches one of the tile indices
                        {
                            if (IsTileValid(area, tile, isLarge)) //if the tile is clear of any obstructions
                            {
                                validTiles.Add(tile);             //add to list of valid spawn tiles
                                break;                            //skip the rest of the indices to avoid adding this tile multiple times
                            }
                        }
                    }
                }
            }
            return(validTiles);
        }
Ejemplo n.º 2
0
        /// <summary>Generates a list of all valid tiles for object spawning in the provided SpawnArea.</summary>
        /// <param name="area">A SpawnArea listing an in-game map name and the valid regions/terrain within it that may be valid spawn points.</param>
        /// <param name="customTileIndex">The list of custom tile indices for this spawn process (e.g. forage or ore generation). Found in the relevant section of Utility.Config.</param>
        /// <param name="isLarge">True if the objects to be spawned are 2x2 tiles in size, otherwise false (1 tile).</param>
        /// <returns>A completed list of all valid tile coordinates for this spawn process in this SpawnArea.</returns>
        public static List <Vector2> GenerateTileList(SpawnArea area, int[] customTileIndex, bool isLarge)
        {
            List <Vector2> validTiles = new List <Vector2>();                  //list of all open, valid tiles for new spawns on the current map

            foreach (string type in area.AutoSpawnTerrainTypes)                //loop to auto-detect valid tiles based on various types of terrain
            {
                if (type.Equals("quarry", StringComparison.OrdinalIgnoreCase)) //add tiles matching the "quarry" tile index list
                {
                    validTiles.AddRange(Utility.GetTilesByIndex(area, Utility.Config.QuarryTileIndex, isLarge));
                }
                else if (type.Equals("custom", StringComparison.OrdinalIgnoreCase)) //add tiles matching the "custom" tile index list
                {
                    validTiles.AddRange(Utility.GetTilesByIndex(area, customTileIndex, isLarge));
                }
                else  //add any tiles with properties matching "type" (e.g. tiles with the "Diggable" property, "Grass" type, etc; if the "type" is "All", this will just add every valid tile)
                {
                    validTiles.AddRange(Utility.GetTilesByProperty(area, type, isLarge));
                }
            }
            foreach (string include in area.IncludeAreas) //check for valid tiles in each "include" zone for the area
            {
                validTiles.AddRange(Utility.GetTilesByVectorString(area, include, isLarge));
            }

            validTiles = validTiles.Distinct().ToList();                                               //remove any duplicate tiles from the list

            foreach (string exclude in area.ExcludeAreas)                                              //check for valid tiles in each "exclude" zone for the area (validity isn't technically relevant here, but simpler to code, and tiles' validity cannot currently change during this process)
            {
                List <Vector2> excludedTiles = Utility.GetTilesByVectorString(area, exclude, isLarge); //get list of valid tiles in the excluded area
                validTiles.RemoveAll(excludedTiles.Contains);                                          //remove any previously valid tiles that match the excluded area
            }

            return(validTiles);
        }
Ejemplo n.º 3
0
            /// <summary>Produces a list of x/y coordinates for valid, open tiles for object spawning at a location (based on a string describing two vectors).</summary>
            /// <param name="area">The SpawnArea describing the current area and its settings.</param>
            /// <param name="vectorString">A string describing two vectors. Parsed into vectors and used to find a rectangular area.</param>
            /// <param name="isLarge">True if the objects to be spawned are 2x2 tiles in size, otherwise false (1 tile).</param>
            /// <returns>A list of Vector2, each representing a valid, open tile for object spawning at the given location.</returns>
            public static List <Vector2> GetTilesByVectorString(SpawnArea area, string vectorString, bool isLarge)
            {
                GameLocation   loc        = Game1.getLocationFromName(area.MapName);                   //variable for the current location being worked on
                List <Vector2> validTiles = new List <Vector2>();                                      //x,y coordinates for tiles that are open & valid for new object placement
                List <Tuple <Vector2, Vector2> > vectorPairs = new List <Tuple <Vector2, Vector2> >(); //pairs of x,y coordinates representing areas on the map (to be scanned for valid tiles)

                //parse the "raw" string representing two coordinates into actual numbers, populating "vectorPairs"
                string[] xyxy = vectorString.Split(new char[] { ',', '/', ';' }); //split the string into separate strings based on various delimiter symbols
                if (xyxy.Length != 4)                                             //if "xyxy" didn't split into the right number of strings, it's probably formatted poorly
                {
                    Monitor.Log($"Issue: This include/exclude area for the {area.MapName} map isn't formatted correctly: \"{vectorString}\"", LogLevel.Info);
                }
                else
                {
                    int[] numbers = new int[4]; //this section will convert "xyxy" into four numbers and store them here
                    bool  success = true;
                    for (int i = 0; i < 4; i++)
                    {
                        if (Int32.TryParse(xyxy[i].Trim(), out numbers[i]) != true) //attempts to store each "xyxy" string as an integer in "numbers"; returns false if it failed
                        {
                            success = false;
                        }
                    }

                    if (success) //everything was successfully parsed, apparently
                    {
                        //convert the numbers to a pair of vectors and add them to the list
                        vectorPairs.Add(new Tuple <Vector2, Vector2>(new Vector2(numbers[0], numbers[1]), new Vector2(numbers[2], numbers[3])));
                    }
                    else
                    {
                        Monitor.Log($"Issue: This include/exclude area for the {area.MapName} map isn't formatted correctly: \"{vectorString}\"", LogLevel.Info);
                    }
                }

                //check the area marked by "vectorPairs" for valid, open tiles and populate "validTiles" with them
                foreach (Tuple <Vector2, Vector2> pair in vectorPairs)
                {
                    for (int y = (int)Math.Min(pair.Item1.Y, pair.Item2.Y); y <= (int)Math.Max(pair.Item1.Y, pair.Item2.Y); y++)     //use the lower Y first, then the higher Y; should define the area regardless of which corners/order the user wrote down
                    {
                        for (int x = (int)Math.Min(pair.Item1.X, pair.Item2.X); x <= (int)Math.Max(pair.Item1.X, pair.Item2.X); x++) //loops for each tile on the map, from the top left (x,y == 0,0) to bottom right, moving horizontally first
                        {
                            Vector2 tile = new Vector2(x, y);
                            if (IsTileValid(area, new Vector2(x, y), isLarge)) //if the tile is clear of any obstructions
                            {
                                validTiles.Add(tile);                          //add to list of valid spawn tiles
                            }
                        }
                    }
                }

                return(validTiles);
            }
Ejemplo n.º 4
0
        //default constructor: configure default forage generation settings
        public ForageSettings()
        {
            Areas = new SpawnArea[] { new SpawnArea("Farm", 0, 3, new string[] { "Grass", "Diggable" }, new string[0], new string[0], "High") }; //a set of "SpawnArea" objects, describing where forage items can spawn on each map
            PercentExtraSpawnsPerForagingLevel = 0;                                                                                              //multiplier to give extra forage per level of foraging skill; default is +0%, since the native game lacks this mechanic

            //the "parentSheetIndex" values for each type of forage item allowed to spawn in each season (the numbers found in ObjectInformation.xnb)
            SpringItemIndex = new int[] { 16, 20, 22, 257 };
            SummerItemIndex = new int[] { 396, 398, 402, 404 };
            FallItemIndex   = new int[] { 281, 404, 420, 422 };
            WinterItemIndex = new int[0];

            CustomTileIndex = new int[0]; //an extra list of tilesheet indices, for use by players who want to make some custom tile detection
        }
Ejemplo n.º 5
0
            /// <summary>Generates a list of all valid tiles for object spawning in the provided SpawnArea.</summary>
            /// <param name="area">A SpawnArea listing an in-game map name and the valid regions/terrain within it that may be valid spawn points.</param>
            /// <param name="quarryTileIndex">The list of quarry tile indices for this spawn process.</param>
            /// <param name="customTileIndex">The list of custom tile indices for this spawn process.</param>
            /// <param name="isLarge">True if the objects to be spawned are 2x2 tiles in size, otherwise false (1 tile).</param>
            /// <returns>A completed list of all valid tile coordinates for this spawn process in this SpawnArea.</returns>
            public static List <Vector2> GenerateTileList(SpawnArea area, InternalSaveData save, int[] quarryTileIndex, int[] customTileIndex, bool isLarge)
            {
                List <Vector2> validTiles = new List <Vector2>();                  //list of all open, valid tiles for new spawns on the current map

                foreach (string type in area.AutoSpawnTerrainTypes)                //loop to auto-detect valid tiles based on various types of terrain
                {
                    if (type.Equals("quarry", StringComparison.OrdinalIgnoreCase)) //add tiles matching the "quarry" tile index list
                    {
                        validTiles.AddRange(Utility.GetTilesByIndex(area, quarryTileIndex, isLarge));
                    }
                    else if (type.Equals("custom", StringComparison.OrdinalIgnoreCase)) //add tiles matching the "custom" tile index list
                    {
                        validTiles.AddRange(Utility.GetTilesByIndex(area, customTileIndex, isLarge));
                    }
                    else  //add any tiles with properties matching "type" (e.g. tiles with the "Diggable" property, "Grass" type, etc; if the "type" is "All", this will just add every valid tile)
                    {
                        validTiles.AddRange(Utility.GetTilesByProperty(area, type, isLarge));
                    }
                }
                foreach (string include in area.IncludeAreas) //check for valid tiles in each "include" zone for the area
                {
                    validTiles.AddRange(Utility.GetTilesByVectorString(area, include, isLarge));
                }

                if (area is LargeObjectSpawnArea objArea && objArea.FindExistingObjectLocations)    //if this area is the large object type and is set to use existing object locations
                {
                    if (save.ExistingObjectLocations.ContainsKey(area.UniqueAreaID))                //if this area has save data for existing locations
                    {
                        foreach (string include in save.ExistingObjectLocations[area.UniqueAreaID]) //check each saved "include" string for the area
                        {
                            validTiles.AddRange(Utility.GetTilesByVectorString(area, include, isLarge));
                        }
                    }
                    else //if this area has not generated any save dat for existing locations yet (note: this *shouldn't* be reachable)
                    {
                        Monitor.Log($"Issue: This area never saved its object location data: {area.UniqueAreaID}", LogLevel.Info);
                        Monitor.Log($"FindExistingObjectLocations will not function for this area. Please report this to the mod's author.", LogLevel.Info);
                    }
                }

                validTiles = validTiles.Distinct().ToList();                                               //remove any duplicate tiles from the list

                foreach (string exclude in area.ExcludeAreas)                                              //check for valid tiles in each "exclude" zone for the area (validity isn't technically relevant here, but simpler to code, and tiles' validity cannot currently change during this process)
                {
                    List <Vector2> excludedTiles = Utility.GetTilesByVectorString(area, exclude, isLarge); //get list of valid tiles in the excluded area
                    validTiles.RemoveAll(excludedTiles.Contains);                                          //remove any previously valid tiles that match the excluded area
                }

                return(validTiles);
            }
Ejemplo n.º 6
0
        /// <summary>Produces a list of x/y coordinates for valid, open tiles for object spawning at a location (based on tile properties, e.g. the "grass" type).</summary>
        /// <param name="area">The SpawnArea describing the current area and its settings.</param>
        /// <param name="type">A string representing the tile property to match, or a special term used for some additional checks.</param>
        /// <param name="isLarge">True if the objects to be spawned are 2x2 tiles in size, otherwise false (1 tile).</param>
        /// <returns>A list of Vector2, each representing a valid, open tile for object spawning at the given location.</returns>
        public static List <Vector2> GetTilesByProperty(SpawnArea area, string type, bool isLarge)
        {
            GameLocation   loc        = Game1.getLocationFromName(area.MapName); //variable for the current location being worked on
            List <Vector2> validTiles = new List <Vector2>();                    //will contain x,y coordinates for tiles that are open & valid for new object placement

            //the following loops should populate a list of valid, open tiles for spawning
            for (int y = 0; y < (loc.Map.DisplayHeight / Game1.tileSize); y++)
            {
                for (int x = 0; x < (loc.Map.DisplayWidth / Game1.tileSize); x++) //loops for each tile on the map, from the top left (x,y == 0,0) to bottom right, moving horizontally first
                {
                    Vector2 tile = new Vector2(x, y);
                    if (type.Equals("all", StringComparison.OrdinalIgnoreCase)) //if the "property" to be matched is "All" (a special exception)
                    {
                        //add any clear tiles, regardless of properties
                        if (IsTileValid(area, tile, isLarge)) //if the tile is clear of any obstructions
                        {
                            validTiles.Add(tile);             //add to list of valid spawn tiles
                        }
                    }
                    if (type.Equals("diggable", StringComparison.OrdinalIgnoreCase))   //if the tile's "Diggable" property matches (case-insensitive)
                    {
                        if (loc.doesTileHaveProperty(x, y, "Diggable", "Back") == "T") //NOTE: the string "T" means "true" for several tile property checks
                        {
                            if (IsTileValid(area, tile, isLarge))                      //if the tile is clear of any obstructions
                            {
                                validTiles.Add(tile);                                  //add to list of valid spawn tiles
                            }
                        }
                    }
                    else //assumed to be checking for a specific value in the tile's "Type" property, e.g. "Grass" or "Dirt"
                    {
                        string currentType = loc.doesTileHaveProperty(x, y, "Type", "Back") ?? ""; //NOTE: this sets itself to a blank (not null) string to avoid null errors when comparing it

                        if (currentType.Equals(type, StringComparison.OrdinalIgnoreCase)) //if the tile's "Type" property matches (case-insensitive)
                        {
                            if (IsTileValid(area, tile, isLarge))                         //if the tile is clear of any obstructions
                            {
                                validTiles.Add(tile);                                     //add to list of valid spawn tiles
                            }
                        }
                    }
                }
            }
            return(validTiles);
        }
Ejemplo n.º 7
0
 public TimedSpawn(SavedObject savedObject, FarmData farmData, SpawnArea spawnArea)
 {
     SavedObject = savedObject;
     FarmData    = farmData;
     SpawnArea   = spawnArea;
 }
Ejemplo n.º 8
0
            /// <summary>Determines whether a specific tile on a map is valid for object placement, using any necessary checks from Stardew's native methods.</summary>
            /// <param name="area">The SpawnArea describing the current area and its settings.</param>
            /// <param name="tile">The tile to be validated for object placement (for a large object, this is effectively its upper left corner).</param>
            /// <param name="isLarge">True if the objects to be spawned are 2x2 tiles in size, otherwise false (1 tile).</param>
            /// <returns>Whether the provided tile is valid for the given area and object size, based on the area's StrictTileChecking setting.</returns>
            public static bool IsTileValid(SpawnArea area, Vector2 tile, bool isLarge)
            {
                GameLocation loc   = Game1.getLocationFromName(area.MapName); //variable for the current location being worked on
                bool         valid = false;


                if (area.StrictTileChecking.Equals("off", StringComparison.OrdinalIgnoreCase) || area.StrictTileChecking.Equals("none", StringComparison.OrdinalIgnoreCase)) //no validation at all
                {
                    valid = true;
                }
                else if (area.StrictTileChecking.Equals("low", StringComparison.OrdinalIgnoreCase)) //low-strictness validation
                {
                    if (isLarge)                                                                    //2x2 tile validation
                    {
                        //if all the necessary tiles for a 2x2 object are *not* blocked by other objects
                        if (!loc.isObjectAtTile((int)tile.X, (int)tile.Y) && !loc.isObjectAtTile((int)tile.X + 1, (int)tile.Y) && !loc.isObjectAtTile((int)tile.X, (int)tile.Y + 1) && !loc.isObjectAtTile((int)tile.X + 1, (int)tile.Y + 1))
                        {
                            valid = true;
                        }
                    }
                    else //single tile validation
                    {
                        if (!loc.isObjectAtTile((int)tile.X, (int)tile.Y)) //if the tile is *not* blocked by another object
                        {
                            valid = true;
                        }
                    }
                }
                else if (area.StrictTileChecking.Equals("medium", StringComparison.OrdinalIgnoreCase)) //medium-strictness validation
                {
                    if (isLarge)                                                                       //2x2 tile validation
                    {
                        //if all the necessary tiles for a 2x2 object are *not* occupied
                        if (!loc.isTileOccupiedForPlacement(tile) && !loc.isTileOccupiedForPlacement(new Vector2(tile.X + 1, tile.Y)) && !loc.isTileOccupiedForPlacement(new Vector2(tile.X, tile.Y + 1)) && !loc.isTileOccupiedForPlacement(new Vector2(tile.X + 1, tile.Y + 1)))
                        {
                            valid = true;
                        }
                    }
                    else //single tile validation
                    {
                        if (!loc.isTileOccupiedForPlacement(tile)) //if the tile is *not* occupied
                        {
                            valid = true;
                        }
                    }
                }
                else //default to "high"-strictness validation
                {
                    if (isLarge) //2x2 tile validation
                    {
                        //if all the necessary tiles for a 2x2 object are *not* occupied
                        if (loc.isTileLocationTotallyClearAndPlaceable(tile) && loc.isTileLocationTotallyClearAndPlaceable(new Vector2(tile.X + 1, tile.Y)) && loc.isTileLocationTotallyClearAndPlaceable(new Vector2(tile.X, tile.Y + 1)) && loc.isTileLocationTotallyClearAndPlaceable(new Vector2(tile.X + 1, tile.Y + 1)))
                        {
                            valid = true;
                        }
                    }
                    else //single tile validation
                    {
                        if (loc.isTileLocationTotallyClearAndPlaceable(tile)) //if the tile is *not* occupied
                        {
                            valid = true;
                        }
                    }
                }

                return(valid);
            }
Ejemplo n.º 9
0
            /// <summary>Generates a list of all valid tiles for object spawning in the provided SpawnArea.</summary>
            /// <param name="area">The SpawnArea that defines which tiles are valid for selection.</param>
            /// <param name="location">The specific game location to be checked for valid tiles.</param>
            /// <param name="quarryTileIndex">The list of valid "quarry" tile indices for this spawn process.</param>
            /// <param name="customTileIndex">The list of valid "custom" tile indices for this spawn process.</param>
            /// <returns>A completed list of all valid tile coordinates for this spawn process in this SpawnArea.</returns>
            public static List <Vector2> GenerateTileList(SpawnArea area, GameLocation location, InternalSaveData save, int[] quarryTileIndex, int[] customTileIndex)
            {
                HashSet <Vector2> validTiles = new HashSet <Vector2>(); //a set of all open, valid tiles for new spawns in the provided area

                //include terrain types
                foreach (string includeType in area.IncludeTerrainTypes)                  //loop to auto-detect valid tiles based on various types of terrain
                {
                    if (includeType == null)                                              //if this terrain type is null
                    {
                        continue;                                                         //skip it
                    }
                    if (includeType.Equals("quarry", StringComparison.OrdinalIgnoreCase)) //add tiles matching the "quarry" tile index list
                    {
                        validTiles.UnionWith(GetTilesByIndex(location, quarryTileIndex));
                    }
                    else if (includeType.Equals("custom", StringComparison.OrdinalIgnoreCase)) //add tiles matching the "custom" tile index list
                    {
                        validTiles.UnionWith(GetTilesByIndex(location, customTileIndex));
                    }
                    else  //add any tiles with properties matching "type" (e.g. tiles with the "Diggable" property, "Grass" type, etc; if the "type" is "All", this will just add every tile)
                    {
                        validTiles.UnionWith(GetTilesByProperty(location, includeType));
                    }
                }

                //include coordinates
                foreach (string includeCoords in area.IncludeCoordinates) //check for tiles in each "include" zone for the area
                {
                    validTiles.UnionWith(GetTilesByVectorString(location, includeCoords));
                }

                //include existing object locations
                if (area is LargeObjectSpawnArea objArea && objArea.FindExistingObjectLocations)    //if this area is the large object type and is set to use existing object locations
                {
                    if (save.ExistingObjectLocations.ContainsKey(area.UniqueAreaID))                //if this area has save data for existing locations
                    {
                        foreach (string include in save.ExistingObjectLocations[area.UniqueAreaID]) //check each saved "include" string for the area
                        {
                            validTiles.UnionWith(GetTilesByVectorString(location, include));
                        }
                    }
                    else //if this area has not generated any save data for existing locations yet (note: this *shouldn't* be reachable)
                    {
                        Monitor.Log($"Issue: This area never saved its object location data: {area.UniqueAreaID}", LogLevel.Info);
                        Monitor.Log($"FindExistingObjectLocations will not function for this area. Please report this to the mod's author.", LogLevel.Info);
                    }
                }

                //exclude terrain types
                foreach (string excludeType in area.ExcludeTerrainTypes)
                {
                    if (excludeType.Equals("quarry", StringComparison.OrdinalIgnoreCase)) //remove tiles matching the "quarry" tile index list
                    {
                        validTiles.ExceptWith(GetTilesByIndex(location, quarryTileIndex));
                    }
                    else if (excludeType.Equals("custom", StringComparison.OrdinalIgnoreCase)) //remove tiles matching the "custom" tile index list
                    {
                        validTiles.ExceptWith(GetTilesByIndex(location, customTileIndex));
                    }
                    else  //remove any tiles with properties matching "type" (e.g. tiles with the "Diggable" property, "Grass" type, etc; if the "type" is "All", this will just remove every tile)
                    {
                        validTiles.ExceptWith(GetTilesByProperty(location, excludeType));
                    }
                }

                //exclude coordinates
                foreach (string excludeCoords in area.ExcludeCoordinates)                   //check for tiles in each "exclude" zone for the area
                {
                    validTiles.ExceptWith(GetTilesByVectorString(location, excludeCoords)); //remove any tiles that match the excluded area
                }

                return(validTiles.ToList()); //convert the set to a list and return it
            }
Ejemplo n.º 10
0
            /// <summary>Checks whether objects should be spawned in a given SpawnArea based on its ExtraConditions settings.</summary>
            /// <param name="area">The SpawnArea to be checked.</param>
            /// <param name="save">The mod's save data for the current farm and config file.</param>
            /// <returns>True if objects are allowed to spawn. False if any extra conditions should prevent spawning.</returns>
            public static bool CheckExtraConditions(SpawnArea area, InternalSaveData save)
            {
                Monitor.Log($"Checking extra conditions for this area...", LogLevel.Trace);

                //check years
                if (area.ExtraConditions.Years != null && area.ExtraConditions.Years.Length > 0)
                {
                    Monitor.Log("Years condition(s) found. Checking...", LogLevel.Trace);

                    bool validYear = false;

                    foreach (string year in area.ExtraConditions.Years)
                    {
                        try                                                                                                                       //watch for errors related to string parsing
                        {
                            if (year.Equals("All", StringComparison.OrdinalIgnoreCase) || year.Equals("Any", StringComparison.OrdinalIgnoreCase)) //if "all" or "any" is listed
                            {
                                validYear = true;
                                break;                                           //skip the rest of the "year" checks
                            }
                            else if (year.Contains("+"))                         //contains a plus, so parse it as a single year & any years after it, e.g. "2+"
                            {
                                string[] split   = year.Split('+');              //split into separate strings around the plus symbol
                                int      minYear = Int32.Parse(split[0].Trim()); //get the number to the left of the plus (trim whitespace)

                                if (minYear <= Game1.year)                       //if the current year is within the specified range
                                {
                                    validYear = true;
                                    break; //skip the rest of the "year" checks
                                }
                            }
                            else if (year.Contains("-"))                            //contains a delimiter, so parse it as a range of years, e.g. "1-10"
                            {
                                string[] split   = year.Split('-');                 //split into separate strings for each delimiter
                                int      minYear = Int32.Parse(split[0].Trim());    //get the first number (trim whitespace)
                                int      maxYear = Int32.Parse(split[1].Trim());    //get the second number (trim whitespace)

                                if (minYear <= Game1.year && maxYear >= Game1.year) //if the current year is within the specified range
                                {
                                    validYear = true;
                                    break; //skip the rest of the "year" checks
                                }
                            }
                            else //parse as a single year, e.g. "1"
                            {
                                int yearNum = Int32.Parse(year.Trim()); //convert to a number

                                if (yearNum == Game1.year) //if it matches the current year
                                {
                                    validYear = true;
                                    break; //skip the rest of the "year" checks
                                }
                            }
                        }
                        catch (Exception)
                        {
                            Monitor.Log($"Issue: This part of the extra condition \"Years\" for the {area.MapName} map isn't formatted correctly: \"{year}\"", LogLevel.Info);
                        }
                    }

                    if (validYear)
                    {
                        Monitor.Log("The current year matched a setting. Spawn allowed.", LogLevel.Trace);
                    }
                    else
                    {
                        Monitor.Log("The current year did NOT match any settings. Spawn disabled.", LogLevel.Trace);
                        return(false);
                    }
                }

                //check seasons
                if (area.ExtraConditions.Seasons != null && area.ExtraConditions.Seasons.Length > 0)
                {
                    Monitor.Log("Seasons condition(s) found. Checking...", LogLevel.Trace);

                    bool validSeason = false;

                    foreach (string season in area.ExtraConditions.Seasons)
                    {
                        if (season.Equals("All", StringComparison.OrdinalIgnoreCase) || season.Equals("Any", StringComparison.OrdinalIgnoreCase)) //if "all" or "any" is listed
                        {
                            validSeason = true;
                            break;                                                                       //skip the rest of the "season" checks
                        }
                        else if (season.Equals(Game1.currentSeason, StringComparison.OrdinalIgnoreCase)) //if the current season is listed
                        {
                            validSeason = true;
                            break; //skip the rest of the "season" checks
                        }
                    }

                    if (validSeason)
                    {
                        Monitor.Log("The current season matched a setting. Spawn allowed.", LogLevel.Trace);
                    }
                    else
                    {
                        Monitor.Log("The current season did NOT match any settings. Spawn disabled.", LogLevel.Trace);
                        return(false); //prevent spawning
                    }
                }

                //check days
                if (area.ExtraConditions.Days != null && area.ExtraConditions.Days.Length > 0)
                {
                    Monitor.Log("Days condition(s) found. Checking...", LogLevel.Trace);

                    bool validDay = false;

                    foreach (string day in area.ExtraConditions.Days)
                    {
                        try                                                                                                                     //watch for errors related to string parsing
                        {
                            if (day.Equals("All", StringComparison.OrdinalIgnoreCase) || day.Equals("Any", StringComparison.OrdinalIgnoreCase)) //if "all" or "any" is listed
                            {
                                validDay = true;
                                break;                                          //skip the rest of the "day" checks
                            }
                            else if (day.Contains("+"))                         //contains a plus, so parse it as a single day & any days after it, e.g. "2+"
                            {
                                string[] split  = day.Split('+');               //split into separate strings around the plus symbol
                                int      minDay = Int32.Parse(split[0].Trim()); //get the number to the left of the plus (trim whitespace)

                                if (minDay <= Game1.dayOfMonth)                 //if the current day is within the specified range
                                {
                                    validDay = true;
                                    break; //skip the rest of the "day" checks
                                }
                            }
                            else if (day.Contains("-"))                                       //contains a delimiter, so parse it as a range of dates, e.g. "1-10"
                            {
                                string[] split  = day.Split('-');                             //split into separate strings for each delimiter
                                int      minDay = Int32.Parse(split[0].Trim());               //get the first number (trim whitespace)
                                int      maxDay = Int32.Parse(split[1].Trim());               //get the second number (trim whitespace)

                                if (minDay <= Game1.dayOfMonth && maxDay >= Game1.dayOfMonth) //if the current day is within the specified range
                                {
                                    validDay = true;
                                    break; //skip the rest of the "day" checks
                                }
                            }
                            else //parse as a single date, e.g. "1" or "25"
                            {
                                int dayNum = Int32.Parse(day.Trim()); //convert to a number

                                if (dayNum == Game1.dayOfMonth) //if it matches the current day
                                {
                                    validDay = true;
                                    break; //skip the rest of the "day" checks
                                }
                            }
                        }
                        catch (Exception)
                        {
                            Monitor.Log($"Issue: This part of the extra condition \"Days\" for the {area.MapName} map isn't formatted correctly: \"{day}\"", LogLevel.Info);
                        }
                    }

                    if (validDay)
                    {
                        Monitor.Log("The current day matched a setting. Spawn allowed.", LogLevel.Trace);
                    }
                    else
                    {
                        Monitor.Log("The current day did NOT match any settings. Spawn disabled.", LogLevel.Trace);
                        return(false); //prevent spawning
                    }
                }

                //check yesterday's weather
                if (area.ExtraConditions.WeatherYesterday != null && area.ExtraConditions.WeatherYesterday.Length > 0)
                {
                    Monitor.Log("Yesterday's Weather condition(s) found. Checking...", LogLevel.Trace);

                    bool validWeather = false;

                    foreach (string weather in area.ExtraConditions.WeatherYesterday)                                                               //for each listed weather name
                    {
                        if (weather.Equals("All", StringComparison.OrdinalIgnoreCase) || weather.Equals("Any", StringComparison.OrdinalIgnoreCase)) //if "all" or "any" is listed
                        {
                            validWeather = true;
                            break; //skip the rest of these checks
                        }

                        switch (save.WeatherForYesterday) //compare to yesterday's weather
                        {
                        case Utility.Weather.Sunny:
                        case Utility.Weather.Festival:     //festival and wedding = sunny, as far as this mod is concerned
                        case Utility.Weather.Wedding:
                            if (weather.Equals("Sun", StringComparison.OrdinalIgnoreCase) || weather.Equals("Sunny", StringComparison.OrdinalIgnoreCase) || weather.Equals("Clear", StringComparison.OrdinalIgnoreCase))
                            {
                                validWeather = true;
                            }
                            break;

                        case Utility.Weather.Rain:
                            if (weather.Equals("Rain", StringComparison.OrdinalIgnoreCase) || weather.Equals("Rainy", StringComparison.OrdinalIgnoreCase) || weather.Equals("Raining", StringComparison.OrdinalIgnoreCase))
                            {
                                validWeather = true;
                            }
                            break;

                        case Utility.Weather.Debris:
                            if (weather.Equals("Wind", StringComparison.OrdinalIgnoreCase) || weather.Equals("Windy", StringComparison.OrdinalIgnoreCase) || weather.Equals("Debris", StringComparison.OrdinalIgnoreCase))
                            {
                                validWeather = true;
                            }
                            break;

                        case Utility.Weather.Lightning:
                            if (weather.Equals("Storm", StringComparison.OrdinalIgnoreCase) || weather.Equals("Stormy", StringComparison.OrdinalIgnoreCase) || weather.Equals("Storming", StringComparison.OrdinalIgnoreCase) || weather.Equals("Lightning", StringComparison.OrdinalIgnoreCase))
                            {
                                validWeather = true;
                            }
                            break;

                        case Utility.Weather.Snow:
                            if (weather.Equals("Snow", StringComparison.OrdinalIgnoreCase) || weather.Equals("Snowy", StringComparison.OrdinalIgnoreCase) || weather.Equals("Snowing", StringComparison.OrdinalIgnoreCase))
                            {
                                validWeather = true;
                            }
                            break;
                        }

                        if (validWeather == true) //if a valid weather condition was listed
                        {
                            break;                //skip the rest of these checks
                        }
                    }


                    if (validWeather)
                    {
                        Monitor.Log("Yesterday's weather matched a setting. Spawn allowed.", LogLevel.Trace);
                    }
                    else
                    {
                        Monitor.Log("Yesterday's weather did NOT match any settings. Spawn disabled.", LogLevel.Trace);
                        return(false); //prevent spawning
                    }
                }

                //check today's weather
                if (area.ExtraConditions.WeatherToday != null && area.ExtraConditions.WeatherToday.Length > 0)
                {
                    Monitor.Log("Today's Weather condition(s) found. Checking...", LogLevel.Trace);

                    bool validWeather = false;

                    foreach (string weather in area.ExtraConditions.WeatherToday)                                                                   //for each listed weather name
                    {
                        if (weather.Equals("All", StringComparison.OrdinalIgnoreCase) || weather.Equals("Any", StringComparison.OrdinalIgnoreCase)) //if "all" or "any" is listed
                        {
                            validWeather = true;
                            break; //skip the rest of these checks
                        }

                        switch (Utility.WeatherForToday()) //compare to today's weather
                        {
                        case Utility.Weather.Sunny:
                        case Utility.Weather.Festival:     //festival and wedding = sunny, as far as this mod is concerned
                        case Utility.Weather.Wedding:
                            if (weather.Equals("Sun", StringComparison.OrdinalIgnoreCase) || weather.Equals("Sunny", StringComparison.OrdinalIgnoreCase) || weather.Equals("Clear", StringComparison.OrdinalIgnoreCase))
                            {
                                validWeather = true;
                            }
                            break;

                        case Utility.Weather.Rain:
                            if (weather.Equals("Rain", StringComparison.OrdinalIgnoreCase) || weather.Equals("Rainy", StringComparison.OrdinalIgnoreCase) || weather.Equals("Raining", StringComparison.OrdinalIgnoreCase))
                            {
                                validWeather = true;
                            }
                            break;

                        case Utility.Weather.Debris:
                            if (weather.Equals("Wind", StringComparison.OrdinalIgnoreCase) || weather.Equals("Windy", StringComparison.OrdinalIgnoreCase) || weather.Equals("Debris", StringComparison.OrdinalIgnoreCase))
                            {
                                validWeather = true;
                            }
                            break;

                        case Utility.Weather.Lightning:
                            if (weather.Equals("Storm", StringComparison.OrdinalIgnoreCase) || weather.Equals("Stormy", StringComparison.OrdinalIgnoreCase) || weather.Equals("Storming", StringComparison.OrdinalIgnoreCase) || weather.Equals("Lightning", StringComparison.OrdinalIgnoreCase))
                            {
                                validWeather = true;
                            }
                            break;

                        case Utility.Weather.Snow:
                            if (weather.Equals("Snow", StringComparison.OrdinalIgnoreCase) || weather.Equals("Snowy", StringComparison.OrdinalIgnoreCase) || weather.Equals("Snowing", StringComparison.OrdinalIgnoreCase))
                            {
                                validWeather = true;
                            }
                            break;
                        }
                        if (validWeather == true) //if a valid weather condition was listed
                        {
                            break;                //skip the rest of these checks
                        }
                    }

                    if (validWeather)
                    {
                        Monitor.Log("Today's weather matched a setting. Spawn allowed.", LogLevel.Trace);
                    }
                    else
                    {
                        Monitor.Log("Today's weather did NOT match any settings. Spawn disabled.", LogLevel.Trace);
                        return(false); //prevent spawning
                    }
                }

                //check tomorrow's weather
                if (area.ExtraConditions.WeatherTomorrow != null && area.ExtraConditions.WeatherTomorrow.Length > 0)
                {
                    Monitor.Log("Tomorrow's Weather condition(s) found. Checking...", LogLevel.Trace);

                    bool validWeather = false;

                    foreach (string weather in area.ExtraConditions.WeatherTomorrow)                                                                //for each listed weather name
                    {
                        if (weather.Equals("All", StringComparison.OrdinalIgnoreCase) || weather.Equals("Any", StringComparison.OrdinalIgnoreCase)) //if "all" or "any" is listed
                        {
                            validWeather = true;
                            break; //skip the rest of these checks
                        }

                        switch (Game1.weatherForTomorrow) //compare to tomorrow's weather
                        {
                        case (int)Utility.Weather.Sunny:
                        case (int)Utility.Weather.Festival:     //festival and wedding = sunny, as far as this mod is concerned
                        case (int)Utility.Weather.Wedding:
                            if (weather.Equals("Sun", StringComparison.OrdinalIgnoreCase) || weather.Equals("Sunny", StringComparison.OrdinalIgnoreCase) || weather.Equals("Clear", StringComparison.OrdinalIgnoreCase))
                            {
                                validWeather = true;
                            }
                            break;

                        case (int)Utility.Weather.Rain:
                            if (weather.Equals("Rain", StringComparison.OrdinalIgnoreCase) || weather.Equals("Rainy", StringComparison.OrdinalIgnoreCase) || weather.Equals("Raining", StringComparison.OrdinalIgnoreCase))
                            {
                                validWeather = true;
                            }
                            break;

                        case (int)Utility.Weather.Debris:
                            if (weather.Equals("Wind", StringComparison.OrdinalIgnoreCase) || weather.Equals("Windy", StringComparison.OrdinalIgnoreCase) || weather.Equals("Debris", StringComparison.OrdinalIgnoreCase))
                            {
                                validWeather = true;
                            }
                            break;

                        case (int)Utility.Weather.Lightning:
                            if (weather.Equals("Storm", StringComparison.OrdinalIgnoreCase) || weather.Equals("Stormy", StringComparison.OrdinalIgnoreCase) || weather.Equals("Storming", StringComparison.OrdinalIgnoreCase) || weather.Equals("Lightning", StringComparison.OrdinalIgnoreCase))
                            {
                                validWeather = true;
                            }
                            break;

                        case (int)Utility.Weather.Snow:
                            if (weather.Equals("Snow", StringComparison.OrdinalIgnoreCase) || weather.Equals("Snowy", StringComparison.OrdinalIgnoreCase) || weather.Equals("Snowing", StringComparison.OrdinalIgnoreCase))
                            {
                                validWeather = true;
                            }
                            break;
                        }

                        if (validWeather == true) //if a valid weather condition was listed
                        {
                            break;                //skip the rest of these checks
                        }
                    }

                    if (validWeather)
                    {
                        Monitor.Log("Tomorrow's weather matched a setting. Spawn allowed.", LogLevel.Trace);
                    }
                    else
                    {
                        Monitor.Log("Tomorrow's weather did NOT match any settings. Spawn disabled.", LogLevel.Trace);
                        return(false); //prevent spawning
                    }
                }

                //check EPU preconditions
                if (area.ExtraConditions.EPUPreconditions != null && area.ExtraConditions.EPUPreconditions.Length > 0)
                {
                    Monitor.Log($"EPU Preconditions found. Checking...", LogLevel.Trace);
                    if (EPUConditionsChecker == null) //if EPU's API is not available
                    {
                        Monitor.LogOnce($"FTM cannot currently access the API for Expanded Preconditions Utility (EPU), but at least one spawn area has EPU preconditions. Those areas will be disabled. Please make sure EPU is installed.", LogLevel.Warn);
                        Monitor.Log($"EPU preconditions could not be checked. Spawn disabled.", LogLevel.Trace);
                        return(false); //prevent spawning
                    }
                    else //if EPU's API is available
                    {
                        try
                        {
                            if (EPUConditionsChecker.CheckConditions(area.ExtraConditions.EPUPreconditions) == true) //if ANY of this area's precondition strings are true
                            {
                                Monitor.Log("At least one EPU precondition string was valid. Spawn allowed.", LogLevel.Trace);
                            }
                            else //if ALL of this area's precondition strings are false
                            {
                                Monitor.Log("All EPU precondition strings were invalid. Spawn disabled.", LogLevel.Trace);
                                return(false); //prevent spawning
                            }
                        }
                        catch (Exception ex)
                        {
                            Monitor.Log($"An error occurred while FTM was using the API for Expanded Preconditions Utility (EPU). Please report this to FTM's developer. Auto-generated error message:", LogLevel.Error);
                            Monitor.Log($"----------", LogLevel.Error);
                            Monitor.Log($"{ex.ToString()}", LogLevel.Error);
                            Monitor.Log($"EPU preconditions could not be checked. Spawn disabled.", LogLevel.Trace);
                            return(false);
                        }
                    }
                }

                //check number of spawns
                //NOTE: it's important that this is the last condition checked, because otherwise it might count down while not actually spawning (i.e. while blocked by another condition)
                if (area.ExtraConditions.LimitedNumberOfSpawns != null)
                {
                    Monitor.Log("Limited Number Of Spawns condition found. Checking...", LogLevel.Trace);
                    if (area.ExtraConditions.LimitedNumberOfSpawns > 0) //if there's at least one spawn day for this area
                    {
                        //if save data already exists for this area
                        if (save.LNOSCounter.ContainsKey(area.UniqueAreaID))
                        {
                            Monitor.Log("Sava data found for this area; checking spawn days counter...", LogLevel.Trace);
                            //if there's still at least one spawn day remaining
                            if ((area.ExtraConditions.LimitedNumberOfSpawns - save.LNOSCounter[area.UniqueAreaID]) > 0)
                            {
                                Monitor.Log($"Spawns remaining (including today): {area.ExtraConditions.LimitedNumberOfSpawns - save.LNOSCounter[area.UniqueAreaID]}. Spawn allowed.", LogLevel.Trace);
                                save.LNOSCounter[area.UniqueAreaID]++; //increment (NOTE: this change needs to be saved at the end of the day)
                            }
                            else //no spawn days remaining
                            {
                                Monitor.Log($"Spawns remaining (including today): {area.ExtraConditions.LimitedNumberOfSpawns - save.LNOSCounter[area.UniqueAreaID]}. Spawn disabled.", LogLevel.Trace);
                                return(false); //prevent spawning
                            }
                        }
                        else //no save file exists for this area; behave as if LNOSCounter == 0
                        {
                            Monitor.Log("No save data found for this area; creating new counter.", LogLevel.Trace);
                            save.LNOSCounter.Add(area.UniqueAreaID, 1); //new counter for this area, starting at 1
                            Monitor.Log($"Spawns remaining (including today): {area.ExtraConditions.LimitedNumberOfSpawns}. Spawn allowed.", LogLevel.Trace);
                        }
                    }
                    else //no spawns remaining
                    {
                        Monitor.Log($"Spawns remaining (including today): {area.ExtraConditions.LimitedNumberOfSpawns}. Spawn disabled.", LogLevel.Trace);
                        return(false); //prevent spawning
                    }
                }

                return(true); //all extra conditions allow for spawning
            }
            /// <summary>Generates specific spawn times for a list of objects and adds them to the Utility.TimedSpawns list.</summary>
            /// <param name="objects">A list of saved objects to be spawned during the current in-game day.</param>
            /// <param name="data">The FarmData from which these saved objects were generated.</param>
            /// <param name="area">The SpawnArea for which these saved objects were generated.</param>
            public static void PopulateTimedSpawnList(List <SavedObject> objects, FarmData data, SpawnArea area)
            {
                List <TimedSpawn> timedSpawns = new List <TimedSpawn>();           //the list of fully processed objects and associated data

                Dictionary <int, int> possibleTimes = new Dictionary <int, int>(); //a dictionary of valid spawn times (keys) and the number of objects assigned to them (values)

                if (area.SpawnTiming == null)                                      //if the SpawnTiming setting is null
                {
                    possibleTimes.Add(600, 0);                                     //spawn everything at 6:00AM
                }
                else
                {
                    for (StardewTime x = area.SpawnTiming.StartTime; x <= area.SpawnTiming.EndTime; x++) //for each 10-minute time from StartTime to EndTime
                    {
                        possibleTimes.Add(x, 0);                                                         //add this time to the list
                    }
                }

                foreach (SavedObject obj in objects)                                                                                                             //for each provided object
                {
                    int index = RNG.Next(0, possibleTimes.Count);                                                                                                //randomly select an index for a valid time
                    obj.SpawnTime = possibleTimes.Keys.ElementAt(index);                                                                                         //assign the time to this object
                    timedSpawns.Add(new TimedSpawn(obj, data, area));                                                                                            //add this object to the processed list
                    possibleTimes[obj.SpawnTime]++;                                                                                                              //increment the number of objects assigned to this time

                    if (area.SpawnTiming.MaximumSimultaneousSpawns.HasValue && area.SpawnTiming.MaximumSimultaneousSpawns.Value <= possibleTimes[obj.SpawnTime]) //if "max spawns" exists and has been reached for this time
                    {
                        possibleTimes.Remove(obj.SpawnTime);                                                                                                     //remove this time from the list
                    }
                    else if (area.SpawnTiming.MinimumTimeBetweenSpawns.HasValue && area.SpawnTiming.MinimumTimeBetweenSpawns.Value > 10)                         //if "time between" exists and is significant
                    {
                        int         between = (area.SpawnTiming.MinimumTimeBetweenSpawns.Value - 10) / 10;                                                       //get the number of other possible times to remove before/after the selected time
                        StardewTime minTime = obj.SpawnTime;                                                                                                     //the earliest time to be removed from the list
                        StardewTime maxTime = obj.SpawnTime;                                                                                                     //the latest time to be removed from the list

                        for (int x = 0; x < between; x++)                                                                                                        //for each adjacent time to be removed
                        {
                            minTime--;                                                                                                                           //select the previous time
                            maxTime++;                                                                                                                           //select the next time
                        }

                        for (int x = possibleTimes.Count - 1; x >= 0; x--) //for each possible time (looping backward for removal purposes)
                        {
                            int time = possibleTimes.Keys.ElementAt(x);
                            if (time != obj.SpawnTime && time >= minTime && time <= maxTime) //if this time isn't the selected time, and is within the range of minTime and maxTime
                            {
                                possibleTimes.Remove(time);                                  //remove it from the list
                            }
                        }
                    }

                    if (possibleTimes.Count <= 0) //if no valid spawn times are left
                    {
                        break;                    //skip the rest of the objects
                    }
                }

                TimedSpawns.Add(timedSpawns); //add the processed list of timed spawns to Utility.TimedSpawns
            }