Ejemplo n.º 1
0
            /// <summary>Check each saved object with an expiration setting, respawning them if they were removed after being saved (e.g. by the weekly forage removal process).</summary>
            /// <param name="save">The save data to the checked.</param>
            public static void ReplaceProtectedSpawns(InternalSaveData save)
            {
                int missing     = 0; //# of objects missing
                int blocked     = 0; //# of objects that could not respawn due to blocked locations
                int respawned   = 0; //# of objects respawned
                int unloaded    = 0; //# of objects skipped due to missing (unloaded) or invalid map names
                int uninstalled = 0; //# of objects skipped due to missing object data, generally caused by removed mods

                foreach (SavedObject saved in save.SavedObjects)
                {
                    if (saved.DaysUntilExpire == null) //if the object's expiration setting is null
                    {
                        continue;                      //skip to the next object
                    }

                    GameLocation location = Game1.getLocationFromName(saved.MapName); //get the object's location

                    if (location == null)                                             //if the map wasn't found
                    {
                        unloaded++;                                                   //increment unloaded tracker
                        continue;                                                     //skip to the next object
                    }

                    if (saved.Type == SavedObject.ObjectType.Monster) //if this is a monster
                    {
                        missing++;                                    //increment missing tracker (note: monsters should always be removed overnight)

                        //this mod should remove all of its monsters overnight, so respawn this monster without checking for its existence
                        int?newID = SpawnMonster(saved.MonType, location, saved.Tile, "[No Area ID: Respawning previously saved monster.]"); //respawn the monster and get its new ID (null if spawn failed)
                        if (newID.HasValue)                                                                                                  //if a monster ID was generated
                        {
                            saved.ID = newID.Value;                                                                                          //update this monster's saved ID
                            respawned++;                                                                                                     //increment respawn tracker
                        }
                        else //if spawn failed (presumably due to obstructions)
                        {
                            blocked++; //increment obstruction tracker
                        }
                    }
                    else if (saved.Type == SavedObject.ObjectType.LargeObject) //if this is a large object
                    {
                        IEnumerable <TerrainFeature> resourceClumps = null;    //a list of large objects at this location
                        if (location is Farm farm)
                        {
                            resourceClumps = farm.resourceClumps.ToList(); //use the farm's clump list
                        }
                        else if (location is MineShaft mine)
                        {
                            resourceClumps = mine.resourceClumps.ToList(); //use the mine's clump list
                        }
                        else
                        {
                            resourceClumps = location.largeTerrainFeatures.OfType <LargeResourceClump>(); //use this location's large resource clump list
                        }

                        bool stillExists = false;                        //does this large object still exist?

                        foreach (TerrainFeature clump in resourceClumps) //for each of this location's large objects
                        {
                            if (clump is ResourceClump smallClump)
                            {
                                if (smallClump.tile.X == saved.Tile.X && smallClump.tile.Y == saved.Tile.Y && smallClump.parentSheetIndex.Value == saved.ID) //if this clump's location & ID match the saved object
                                {
                                    stillExists = true;
                                    break; //stop searching the clump list
                                }
                            }
                            else if (clump is LargeResourceClump largeClump)
                            {
                                if (largeClump.Clump.Value.tile.X == saved.Tile.X && largeClump.Clump.Value.tile.Y == saved.Tile.Y && largeClump.Clump.Value.parentSheetIndex.Value == saved.ID) //if this clump's location & ID match the saved object
                                {
                                    stillExists = true;
                                    break; //stop searching the clump list
                                }
                            }
                        }

                        if (!stillExists)                                               //if the object no longer exists
                        {
                            missing++;                                                  //increment missing tracker

                            if (IsTileValid(location, saved.Tile, saved.Size, "High"))  //if the object's tile is valid for large object placement (defaulting to "high" strictness)
                            {
                                SpawnLargeObject(saved.ID.Value, location, saved.Tile); //respawn the object
                                respawned++;                                            //increment respawn tracker
                            }
                            else //if the object's tile is invalid
                            {
                                blocked++; //increment obstruction tracker
                            }
                        }
                    }
                    else if (saved.Type == SavedObject.ObjectType.Item) //if this is a forage item
                    {
                        missing++;                                      //increment missing tracker (note: items should always be removed overnight)

                        //this mod should remove all of its forage items overnight, so respawn this item without checking for its existence
                        if (IsTileValid(location, saved.Tile, new Point(1, 1), "Medium") && !location.terrainFeatures.ContainsKey(saved.Tile)) //if the item's tile is clear enough to respawn
                        {
                            //update this item's ID, in case it changed due to other mods
                            string[] categoryAndName = saved.Name.Split(':');
                            int?     newID           = GetItemID(categoryAndName[0], categoryAndName[1]);

                            if (newID.HasValue)                           //if a new ID was successfully generated
                            {
                                respawned++;                              //increment respawn tracker
                                saved.ID = newID;                         //save the new ID
                                SpawnForage(saved, location, saved.Tile); //respawn the item
                            }
                            else //if a new ID could not be generated
                            {
                                uninstalled++; //increment uninstalled mod tracker
                                Monitor.LogOnce($"Couldn't find a valid ID for a previously saved forage item. Item name: {saved.Name}", LogLevel.Trace);
                            }
                        }
                        else //if this object's tile is obstructed
                        {
                            blocked++; //increment obstruction tracker
                        }
                    }
                    else if (saved.Type == SavedObject.ObjectType.Container) //if this is a container
                    {
                        missing++;                                           //increment missing tracker (note: chests should always be removed overnight)

                        //this mod should remove all of its containers overnight, so respawn this container without checking for its existence
                        if (IsTileValid(location, saved.Tile, new Point(1, 1), "Medium")) //if the container's tile is clear enough to respawn
                        {
                            respawned++;                                                  //increment respawn tracker
                            SpawnForage(saved, location, saved.Tile);                     //respawn the container
                        }
                        else //if this object's tile is obstructed
                        {
                            blocked++; //increment obstruction tracker
                        }
                    }
                    else if (saved.Type == SavedObject.ObjectType.DGA)                                                         //if this is a DGA item
                    {
                        StardewValley.Object realObject    = location.getObjectAtTile((int)saved.Tile.X, (int)saved.Tile.Y);   //get the object at the saved location (if any)
                        Furniture            realFurniture = location.GetFurnitureAt(saved.Tile);                              //get the furniture at the saved location (if any)
                        bool featureExists = location.terrainFeatures.TryGetValue(saved.Tile, out TerrainFeature realFeature); //try to get a terrain feature at this location

                        if (DGAItemAPI == null)                                                                                //if DGA isn't available
                        {
                            uninstalled++;                                                                                     //increment uninstalled mod tracker
                            Monitor.LogOnce($"The interface for Dynamic Game Assets (DGA) is unavailable, so a DGA item couldn't be respawned from save data.", LogLevel.Trace);
                        }
                        else if ((realObject == null || DGAItemAPI.GetDGAItemId(realObject) != saved.Name) && //if a matching DGA object is NOT here
                                 (realFurniture == null || DGAItemAPI.GetDGAItemId(realFurniture) != saved.Name) && //AND a matching DGA furniture is NOT here
                                 (!featureExists || realFeature is not PlacedItem placed || placed.Item == null || DGAItemAPI.GetDGAItemId(placed.Item) != saved.Name)) //AND a matching DGA item is NOT here
                        {
                            missing++;                                                                                                                                  //increment missing object tracker

                            if (IsTileValid(location, saved.Tile, new Point(1, 1), "Medium"))                                                                           //if the item's tile is clear enough to respawn
                            {
                                respawned++;                                                                                                                            //increment respawn tracker
                                SpawnForage(saved, location, saved.Tile);                                                                                               //respawn the DGA item
                            }
                            else //if the object's tile is obstructed
                            {
                                blocked++; //increment obstruction tracker
                            }
                        }
                    }
                    else //if this is forage or ore
                    {
                        StardewValley.Object realObject = location.getObjectAtTile((int)saved.Tile.X, (int)saved.Tile.Y); //get the object at the saved location

                        if (realObject == null)                                               //if the object no longer exists
                        {
                            missing++;                                                        //increment missing object tracker

                            if (IsTileValid(location, saved.Tile, new Point(1, 1), "Medium")) //if the object's tile is clear enough to respawn
                            {
                                if (saved.Type == SavedObject.ObjectType.Object)              //if this is a forage object
                                {
                                    if (saved.Name != null)                                   //if this forage was originally assigned a name
                                    {
                                        //update this forage's ID, in case it changed due to other mods

                                        if (saved.Name.Contains(':')) //if this is "category:name"
                                        {
                                            string[] categoryAndName = saved.Name.Split(':');
                                            saved.ID = GetItemID(categoryAndName[0], categoryAndName[1]);
                                        }
                                        else //if this is just an object name
                                        {
                                            saved.ID = GetItemID("object", saved.Name);
                                        }
                                    }

                                    if (saved.ID.HasValue)                        //if a valid ID was found for this object
                                    {
                                        respawned++;                              //increment respawn tracker
                                        SpawnForage(saved, location, saved.Tile); //respawn it
                                    }
                                    else
                                    {
                                        uninstalled++; //increment uninstalled mod tracker
                                        Monitor.LogOnce($"Couldn't find a valid ID for a previously saved forage object. Object name: {saved.Name}", LogLevel.Trace);
                                    }
                                }
                                else //if this is ore
                                {
                                    respawned++; //increment respawn tracker
                                    SpawnOre(saved.Name, location, saved.Tile); //respawn it
                                }
                            }
                            else //if the object's tile is occupied
                            {
                                blocked++; //increment obstruction tracker
                            }
                        }
                    }
                }

                Monitor.Log($"Missing objects: {missing}. Respawned: {respawned}. Not respawned due to obstructions: {blocked}. Skipped due to missing maps: {unloaded}. Skipped due to missing item types: {uninstalled}.", LogLevel.Trace);
            }
            /// <summary>Check each saved object's expiration data, updating counters & removing missing/expired objects from the data and custom classes from the game world.</summary>
            /// <param name="save">The save data to the checked.</param>
            /// <param name="endOfDay">If false, expiration dates will be ignored. Used to temporarily remove custom classes during the day.</param>
            public static void ProcessObjectExpiration(InternalSaveData save, bool endOfDay = true)
            {
                List <SavedObject> objectsToRemove = new List <SavedObject>();                         //objects to remove from saved data after processing (note: do not remove them while looping through them)

                foreach (SavedObject saved in save.SavedObjects)                                       //for each saved object & expiration countdown
                {
                    if (saved.DaysUntilExpire == null && saved.Type != SavedObject.ObjectType.Monster) //if the object's expiration setting is null & it's not a monster
                    {
                        Monitor.VerboseLog($"Removing object data saved with a null expiration setting. Type: {saved.Type.ToString()}. ID: {saved.ID}. Location: {saved.MapName}.");
                        objectsToRemove.Add(saved); //mark this for removal from save
                        continue;                   //skip to the next object
                    }

                    //if this saved object has an expiration setting, AND the target map is a known temporary location
                    if (saved.DaysUntilExpire.HasValue &&
                        (saved.MapName.StartsWith("UndergroundMine", StringComparison.OrdinalIgnoreCase) || //mine level
                         saved.MapName.StartsWith("VolcanoDungeon", StringComparison.OrdinalIgnoreCase))) //volcano level
                    {
                        saved.DaysUntilExpire = 1;                                                        //force this object to expire below
                    }

                    GameLocation location = Game1.getLocationFromName(saved.MapName); //get the saved object's location

                    if (location == null)                                             //if this isn't a valid map
                    {
                        Monitor.VerboseLog($"Removing object data saved for a missing location. Type: {saved.Type.ToString()}. ID: {saved.ID}. Location: {saved.MapName}.");
                        objectsToRemove.Add(saved); //mark this for removal from save
                        continue;                   //skip to the next object
                    }

                    if (saved.Type == SavedObject.ObjectType.Monster)                                //if this is a monster
                    {
                        bool stillExists = false;                                                    //does this monster still exist?

                        for (int x = location.characters.Count - 1; x >= 0; x--)                     //for each character at this location (looping backward for removal purposes)
                        {
                            if (location.characters[x] is Monster monster && monster.id == saved.ID) //if this is a monster with an ID that matches the saved ID
                            {
                                stillExists = true;
                                if (endOfDay)                                                        //if expirations should be processed
                                {
                                    if (saved.DaysUntilExpire == 1 || saved.DaysUntilExpire == null) //if this should expire tonight (including monsters generated without expiration settings)
                                    {
                                        Monitor.VerboseLog($"Removing expired object. Type: {saved.Type.ToString()}. ID: {saved.ID}. Location: {saved.MapName}.");
                                        objectsToRemove.Add(saved);     //mark this for removal from save
                                    }
                                    else if (saved.DaysUntilExpire > 1) //if the object should expire, but not tonight
                                    {
                                        saved.DaysUntilExpire--;        //decrease counter by 1
                                    }
                                }

                                if (saved.MonType != null && saved.MonType.Settings.ContainsKey("PersistentHP") && (bool)saved.MonType.Settings["PersistentHP"]) //if the PersistentHP setting is enabled for this monster
                                {
                                    saved.MonType.Settings["CurrentHP"] = monster.Health;                                                                        //save this monster's current HP
                                }

                                location.characters.RemoveAt(x); //remove this monster from the location, regardless of expiration
                                break;                           //stop searching the character list
                            }
                        }

                        if (!stillExists) //if this monster no longer exists
                        {
                            Monitor.VerboseLog($"Removing missing object. Type: {saved.Type.ToString()}. ID: {saved.ID}. Location: {saved.MapName}.");
                            objectsToRemove.Add(saved); //mark this for removal from save
                        }
                    }
                    else if (saved.Type == SavedObject.ObjectType.ResourceClump) //if this is a resource clump
                    {
                        IEnumerable <TerrainFeature> resourceClumps = null;      //a list of resource clumps at this location
                        if (location is Farm farm)
                        {
                            resourceClumps = farm.resourceClumps.ToList(); //use the farm's clump list
                        }
                        else if (location is MineShaft mine)
                        {
                            resourceClumps = mine.resourceClumps.ToList(); //use the mine's clump list
                        }
                        else
                        {
                            resourceClumps = location.largeTerrainFeatures.OfType <LargeResourceClump>(); //use this location's large resource clump list
                        }

                        TerrainFeature existingObject = null;            //the in-game object, if it currently exists

                        foreach (TerrainFeature clump in resourceClumps) //for each of this location's large objects
                        {
                            if (clump is ResourceClump smallClump)
                            {
                                if (smallClump.tile.X == saved.Tile.X && smallClump.tile.Y == saved.Tile.Y && smallClump.parentSheetIndex.Value == saved.ID) //if this clump's location & ID match the saved object
                                {
                                    existingObject = smallClump;
                                    break; //stop searching the clump list
                                }
                            }
                            else if (clump is LargeResourceClump largeClump)
                            {
                                if (largeClump.Clump.Value.tile.X == saved.Tile.X && largeClump.Clump.Value.tile.Y == saved.Tile.Y && largeClump.Clump.Value.parentSheetIndex.Value == saved.ID) //if this clump's location & ID match the saved object
                                {
                                    existingObject = largeClump;
                                    break; //stop searching the clump list
                                }
                            }
                        }

                        if (existingObject != null)             //if the object still exists
                        {
                            if (endOfDay)                       //if expirations should be processed
                            {
                                if (saved.DaysUntilExpire == 1) //if the object should expire tonight
                                {
                                    Monitor.VerboseLog($"Removing expired object. Type: {saved.Type.ToString()}. ID: {saved.ID}. Location: {saved.Tile.X},{saved.Tile.Y} ({saved.MapName}).");

                                    if (existingObject is ResourceClump clump) //if this is NOT a custom class that always needs removal
                                    {
                                        if (location is Farm farmLoc)
                                        {
                                            farmLoc.resourceClumps.Remove(clump); //remove this object from the farm's resource clumps list
                                        }
                                        else if (location is MineShaft mineLoc)
                                        {
                                            mineLoc.resourceClumps.Remove(clump); //remove this object from the mine's resource clumps list
                                        }
                                    }

                                    objectsToRemove.Add(saved);     //mark object for removal from save
                                }
                                else if (saved.DaysUntilExpire > 1) //if the object should expire, but not tonight
                                {
                                    saved.DaysUntilExpire--;        //decrease counter by 1
                                }
                            }

                            if (existingObject is LargeResourceClump largeClump)  //if this is a custom class that always needs removal
                            {
                                location.largeTerrainFeatures.Remove(largeClump); //remove this object from the large terrain features list (NOTE: this must be done even for unexpired LargeResourceClumps to avoid SDV save errors)
                            }
                        }
                        else //if the object no longer exists
                        {
                            Monitor.VerboseLog($"Removing missing object. Type: {saved.Type.ToString()}. ID: {saved.ID}. Location: {saved.MapName}.");
                            objectsToRemove.Add(saved); //mark object for removal from save
                        }
                    }
                    else if (saved.Type == SavedObject.ObjectType.Item) //if this is a forage item, i.e. "debris" containing an item
                    {
                        bool stillExists = false;                       //does this item still exist?

                        //if a PlacedItem terrain feature exists at the saved tile & contains an item with a matching name
                        if (location.terrainFeatures.ContainsKey(saved.Tile) && location.terrainFeatures[saved.Tile] is PlacedItem placedItem && placedItem.Item?.ParentSheetIndex == saved.ID.Value)
                        {
                            stillExists = true;
                            location.terrainFeatures.Remove(saved.Tile);                         //remove this placed item, regardless of expiration

                            if (endOfDay)                                                        //if expirations should be processed
                            {
                                if (saved.DaysUntilExpire == 1 || saved.DaysUntilExpire == null) //if this should expire tonight
                                {
                                    Monitor.VerboseLog($"Removing expired object. Type: {saved.Type.ToString()}. Name: {placedItem.Item?.Name}. Location: {saved.MapName}.");
                                    objectsToRemove.Add(saved);     //mark this for removal from save
                                }
                                else if (saved.DaysUntilExpire > 1) //if this should expire, but not tonight
                                {
                                    saved.DaysUntilExpire--;        //decrease counter by 1
                                }
                            }
                        }

                        if (!stillExists) //if this item no longer exists
                        {
                            Monitor.VerboseLog($"Removing missing object. Type: {saved.Type.ToString()}. ID: {saved.ID}. Location: {saved.MapName}.");
                            objectsToRemove.Add(saved); //mark this for removal from save
                        }
                    }
                    else if (saved.Type == SavedObject.ObjectType.Container)                               //if this is a container
                    {
                        if (location.Objects.TryGetValue(saved.Tile, out StardewValley.Object realObject)) //if an object exists in the saved location
                        {
                            bool sameContainerCategory = false;
                            switch (saved.ConfigItem?.Category.ToLower()) //compare the saved object's category to this object's class
                            {
                            case "barrel":
                            case "barrels":
                            case "breakable":
                            case "breakables":
                            case "crate":
                            case "crates":
                                if (realObject is BreakableContainerFTM)
                                {
                                    sameContainerCategory = true;
                                }
                                break;

                            case "buried":
                            case "burieditem":
                            case "burieditems":
                            case "buried item":
                            case "buried items":
                                if (realObject is BuriedItems)
                                {
                                    sameContainerCategory = true;
                                }
                                break;

                            case "chest":
                            case "chests":
                                if (realObject is Chest)
                                {
                                    sameContainerCategory = true;
                                }
                                break;
                            }

                            if (sameContainerCategory)                                           //if the real object matches the saved object's category
                            {
                                if (realObject is Chest chest)                                   //if this is a chest
                                {
                                    while (chest.items.Count < saved.ConfigItem?.Contents.Count) //while this chest has less items than the saved object's "contents"
                                    {
                                        saved.ConfigItem.Contents.RemoveAt(0);                   //remove a missing item from the ConfigItem's contents (note: chests output the item at index 0 when used)
                                    }
                                }

                                realObject.CanBeGrabbed = true;           //workaround for certain objects being ignored by the removeObject method
                                location.removeObject(saved.Tile, false); //remove this container from the location, regardless of expiration

                                if (endOfDay)                             //if expirations should be processed
                                {
                                    if (saved.DaysUntilExpire == 1)       //if the object should expire tonight
                                    {
                                        Monitor.VerboseLog($"Removing expired container. Type: {saved.Type.ToString()}. Category: {saved.ConfigItem?.Category}. Location: {saved.Tile.X},{saved.Tile.Y} ({saved.MapName}).");

                                        objectsToRemove.Add(saved);     //mark object for removal from save
                                    }
                                    else if (saved.DaysUntilExpire > 1) //if the object should expire, but not tonight
                                    {
                                        saved.DaysUntilExpire--;        //decrease counter by 1
                                    }
                                }
                            }
                            else //if the real object does NOT match the saved object's category
                            {
                                Monitor.VerboseLog($"Removing missing object. Type: {saved.Type.ToString()}. Category: {saved.ConfigItem?.Category}. Location: {saved.MapName}.");
                                objectsToRemove.Add(saved); //mark object for removal from save
                            }
                        }
                        else //if the object no longer exists
                        {
                            Monitor.VerboseLog($"Removing missing object. Type: {saved.Type.ToString()}. Category: {saved.ConfigItem?.Category}. Location: {saved.MapName}.");
                            objectsToRemove.Add(saved); //mark object for removal from save
                        }
                    }
                    else if (saved.Type == SavedObject.ObjectType.DGA) //if this is a DGA item
                    {
                        if                                             //if a matching PlacedItem exists here
                        (
                            location.terrainFeatures.ContainsKey(saved.Tile) && //if this tile has a features
                            location.terrainFeatures[saved.Tile] is PlacedItem placedItem && //and it's a placed item
                            placedItem.Item != null && //and it isn't empty
                            DGAItemAPI?.GetDGAItemId(placedItem.Item) == saved.Name    //and the contained item matches the saved name (according to DGA's API)
                        )
                        {
                            location.terrainFeatures.Remove(saved.Tile);                         //remove this placed item, regardless of expiration

                            if (endOfDay)                                                        //if expirations should be processed
                            {
                                if (saved.DaysUntilExpire == 1 || saved.DaysUntilExpire == null) //if this should expire tonight
                                {
                                    Monitor.VerboseLog($"Removing expired object. Type: DGA item. Name: {saved.Name}. Location: {saved.Tile.X},{saved.Tile.Y} ({saved.MapName}).");
                                    objectsToRemove.Add(saved);     //mark this for removal from save
                                }
                                else if (saved.DaysUntilExpire > 1) //if this should expire, but not tonight
                                {
                                    saved.DaysUntilExpire--;        //decrease counter by 1
                                }
                            }
                        }
                        else if (location.GetFurnitureAt(saved.Tile) is Furniture realFurniture && DGAItemAPI?.GetDGAItemId(realFurniture) == saved.Name) //if matching furniture exists here
                        {
                            location.furniture.Remove(realFurniture);                                                                                     //remove this furniture, regardless of expiration

                            if (endOfDay)                                                                                                                 //if expirations should be processed
                            {
                                if (saved.DaysUntilExpire == 1 || saved.DaysUntilExpire == null)                                                          //if this should expire tonight
                                {
                                    Monitor.VerboseLog($"Removing expired object. Type: DGA furniture. Name: {saved.Name}. Location: {saved.Tile.X},{saved.Tile.Y} ({saved.MapName}).");
                                    objectsToRemove.Add(saved);     //mark this for removal from save
                                }
                                else if (saved.DaysUntilExpire > 1) //if this should expire, but not tonight
                                {
                                    saved.DaysUntilExpire--;        //decrease counter by 1
                                }
                            }
                        }