예제 #1
0
 /// <summary>Attempts to generate and place an item using the <see cref="Utility.DGAItemAPI"/> interface.</summary>
 /// <param name="forage">The SavedObject containing this forage's information.</param>
 /// <param name="location">The GameLocation where the forage should be spawned.</param>
 /// <param name="tile">The x/y coordinates of the tile where the ore should be spawned.</param>
 /// <returns>True if the item spawned successfully; false otherwise.</returns>
 private static bool SpawnDGAItem(SavedObject forage, GameLocation location, Vector2 tile)
 {
     try
     {
         object rawDGA = DGAItemAPI.SpawnDGAItem(forage.Name); //try to create this item with DGA's API
         if (rawDGA is Furniture furnitureDGA)                 //if the resulting item is furniture
         {
             Monitor.VerboseLog($"Spawning DGA forage furniture. Name: {forage.Name}. Location: {tile.X},{tile.Y} ({location.Name}).");
             furnitureDGA.TileLocation = tile;
             Rectangle originalBoundingBox = furnitureDGA.boundingBox.Value;                                                                            //get "original" bounding box
             furnitureDGA.boundingBox.Value = new Rectangle((int)tile.X * 64, (int)tile.Y * 64, originalBoundingBox.Width, originalBoundingBox.Height); //adjust for tile position
             furnitureDGA.updateDrawPosition();
             location.furniture.Add(furnitureDGA);                                                                                                      //add the furniture to this location
             return(true);
         }
         else if (rawDGA is StardewValley.Object objectDGA) //if the resulting item is a SDV object (i.e. can be treated like normal forage)
         {
             Monitor.VerboseLog($"Spawning DGA forage object. Name: {forage.Name}. Location: {tile.X},{tile.Y} ({location.Name}).");
             objectDGA.IsSpawnedObject = true;
             return(location.dropObject(objectDGA, tile * 64f, Game1.viewport, true, null)); //attempt to place the object and return success/failure
         }
         else if (rawDGA is Item itemDGA)                                                    //if the resulting item is any other type of Item (i.e. can be treated as a PlacedItem)
         {
             if (location.terrainFeatures.ContainsKey(tile))                                 //if a terrain feature already exists on this tile
             {
                 return(false);                                                              //fail to spawn
             }
             Monitor.VerboseLog($"Spawning DGA forage item. Name: {forage.Name}. Location: {tile.X},{tile.Y} ({location.Name}).");
             PlacedItem placed = new PlacedItem(tile, itemDGA); //create a terrainfeature containing the item
             location.terrainFeatures.Add(tile, placed);        //add the placed item to this location
             return(true);
         }
         else if (rawDGA != null) //if DGA spawned an item, but it isn't a recognized type
         {
             Monitor.Log("Dynamic Game Assets (DGA) created an item, but FTM doesn't recognize its type. This may be caused by the item or a problem with FTM's logic.", LogLevel.Warn);
             Monitor.Log($"Item name: {forage.Name}", LogLevel.Warn);
             Monitor.Log($"Item type (C# code): {rawDGA.GetType()?.Name ?? "null"}", LogLevel.Warn);
             return(false);
         }
         else //if DGA did not spawn an item
         {
             Monitor.Log("The SpawnForage method failed to generate a Dynamic Game Assets (DGA) item. This may be caused by a problem with this mod's logic. Please report this to FTM's developer if possible.", LogLevel.Warn);
             Monitor.Log($"Item name: {forage.Name}", LogLevel.Warn);
             return(false);
         }
     }
     catch (Exception ex)
     {
         Monitor.Log($"An error occurred while spawning a Dynamic Game Assets (DGA) item.", LogLevel.Warn);
         Monitor.Log($"Item name: \"{forage.Name}\"", LogLevel.Warn);
         Monitor.Log($"The affected item will be skipped. The auto-generated error message has been added to the log.", LogLevel.Warn);
         Monitor.Log($"----------", LogLevel.Trace);
         Monitor.Log($"{ex.ToString()}", LogLevel.Trace);
         return(false);
     }
 }
예제 #2
0
            /// <summary>Generates an item described by a saved object.</summary>
            /// <param name="save">A saved object descibing an item.</param>
            /// <param name="tile">The object's intended tile location. Generally necessary for items derived from StardewValley.Object.</param>
            public static Item CreateItem(SavedObject save, Vector2 tile = default(Vector2))
            {
                switch (save.Type) //check the object's type
                {
                case SavedObject.ObjectType.Object:
                case SavedObject.ObjectType.Item:
                case SavedObject.ObjectType.Container:
                case SavedObject.ObjectType.DGA:
                    //these are valid item types
                    break;

                default:
                    Monitor.Log($"Failed to create an item. Saved object does not appear to be an item.", LogLevel.Debug);
                    Monitor.Log($"Item name: {save.Name}", LogLevel.Debug);
                    return(null);
                }

                if (!save.ID.HasValue && save.Type != SavedObject.ObjectType.Container && save.Type != SavedObject.ObjectType.DGA) //if this save doesn't have an ID (and isn't a container or a DGA item)
                {
                    Monitor.Log("Failed to create an item. Saved object contained no ID.", LogLevel.Debug);
                    Monitor.Log($"Item name: {save.Name}", LogLevel.Debug);
                    return(null);
                }

                Item       item       = null;            //the item to be generated
                ConfigItem configItem = save.ConfigItem; //the ConfigItem class describing the item (null if unavailable)

                //parse container contents, if applicable
                List <Item> contents = new List <Item>();

                if (save.Type == SavedObject.ObjectType.Container)                                                //if this is a container
                {
                    string             areaID       = $"[unknown; parsing chest contents at {save.MapName}]";     //placeholder string; this method has no easy access to the areaID that created a given item
                    List <SavedObject> contentSaves = ParseSavedObjectsFromItemList(configItem.Contents, areaID); //parse the contents into saved objects for validation purposes

                    foreach (SavedObject contentSave in contentSaves)                                             //for each successfully parsed save
                    {
                        Item content = CreateItem(contentSave);                                                   //call this method recursively to create this item
                        if (content != null)                                                                      //if this item was created successfully
                        {
                            contents.Add(content);                                                                //add it to the contents list
                        }
                    }
                }

                string category = "item";

                if (configItem != null && configItem.Category != null)
                {
                    category = configItem.Category.ToLower();
                }

                switch (category) //based on the category
                {
                case "barrel":
                case "barrels":
                    item = new BreakableContainerFTM(tile, contents, true);     //create a mineshaft-style breakable barrel with the given contents
                    break;

                case "bigcraftable":
                case "bigcraftables":
                case "big craftable":
                case "big craftables":
                    item = new StardewValley.Object(tile, save.ID.Value, false);     //create an object as a "big craftable" item
                    break;

                case "boot":
                case "boots":
                    item = new Boots(save.ID.Value);
                    break;

                case "breakable":
                case "breakables":
                    bool barrel = RNG.Next(0, 2) == 0 ? true : false;     //randomly select whether this is a barrel or crate
                    if (configItem != null)
                    {
                        //rewrite the category to save the selection
                        if (barrel)
                        {
                            configItem.Category = "barrel";
                        }
                        else
                        {
                            configItem.Category = "crate";
                        }
                    }
                    item = new BreakableContainerFTM(tile, contents, barrel);     //create a mineshaft-style breakable container with the given contents
                    break;

                case "buried":
                case "burieditem":
                case "burieditems":
                case "buried item":
                case "buried items":
                    item = new BuriedItems(tile, contents);     //create an item burial location with the given contents
                    break;

                case "chest":
                case "chests":
                    item = new Chest(0, contents, tile, false, 0);     //create a mineshaft-style chest with the given contents
                    break;

                case "cloth":
                case "clothes":
                case "clothing":
                case "clothings":
                    item = new Clothing(save.ID.Value);
                    break;

                case "crate":
                case "crates":
                    item = new BreakableContainerFTM(tile, contents, false);     //create a mineshaft-style breakable crate with the given contents
                    break;

                case "dga":
                    try
                    {
                        object rawDGA = DGAItemAPI.SpawnDGAItem(save.Name); //create an item with DGA's API

                        if (rawDGA is Item itemDGA)                         //if this is a non-null Item
                        {
                            item = itemDGA;                                 //use it
                        }
                        else
                        {
                            Monitor.Log("Failed to create an item. Dynamic Game Assets (DGA) item was null or an unrecognized type.", LogLevel.Debug);
                            Monitor.Log($"Item name: {save.Name}", LogLevel.Debug);
                            return(null);
                        }
                    }
                    catch (Exception ex)
                    {
                        Monitor.LogOnce($"Error spawning a Dynamic Game Assets (DGA) item. The auto-generated error message has been added to the log.", LogLevel.Info);
                        Monitor.Log($"----------", LogLevel.Trace);
                        Monitor.Log($"{ex.ToString()}", LogLevel.Trace);
                        return(null);
                    }
                    break;

                case "furniture":
                    item = new Furniture(save.ID.Value, tile);
                    break;

                case "hat":
                case "hats":
                    item = new Hat(save.ID.Value);
                    break;

                case "object":     //treat objects as items when creating them as Items
                case "objects":
                case "item":
                case "items":
                    item = new StardewValley.Object(tile, save.ID.Value, 1);     //create an object with the preferred constructor for "held" or "dropped" items
                    break;

                case "ring":
                case "rings":
                    item = new Ring(save.ID.Value);
                    break;

                case "weapon":
                case "weapons":
                    item = new MeleeWeapon(save.ID.Value);
                    break;
                }

                if (item == null) //if no item could be generated
                {
                    Monitor.Log("Failed to create an item. Category setting was not recognized.", LogLevel.Debug);
                    Monitor.Log($"Item Category: {category}", LogLevel.Debug);
                    return(null);
                }

                if (configItem?.Stack > 1)               //if this item has a custom stack setting
                {
                    item.Stack = configItem.Stack.Value; //apply it
                }

                if (save.ID.HasValue)                      //if this object type uses an ID
                {
                    item.ParentSheetIndex = save.ID.Value; //manually set this index value, due to it being ignored by some item subclasses
                }

                return(item);
            }
예제 #3
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
                                }
                            }
                        }
예제 #5
0
            /// <summary>Uses a ConfigItem to create a saved object representing an item.</summary>
            /// <param name="item">The ConfigItem class describing the item.</param>
            /// <param name="areaID">The UniqueAreaID of the related SpawnArea. Required for log messages.</param>
            /// <returns>A saved object representing the designated item. Null if creation failed.</returns>
            private static SavedObject CreateSavedObject(ConfigItem item, string areaID = "")
            {
                switch (item.Type)
                {
                case SavedObject.ObjectType.Object:
                case SavedObject.ObjectType.Item:
                case SavedObject.ObjectType.Container:
                case SavedObject.ObjectType.DGA:
                    //these are valid item types
                    break;

                default:
                    Monitor.Log($"An area's item list contains a complex item with a type that is not recognized.", LogLevel.Info);
                    Monitor.Log($"Affected spawn area: \"{areaID}\"", LogLevel.Info);
                    Monitor.Log($"Item type: \"{item.Type}\"", LogLevel.Info);
                    Monitor.Log($"This is likely due to a design error in the mod's code. Please report this to the mod's developer. The affected item will be skipped.", LogLevel.Info);
                    return(null);
                }

                if (item.Contents != null)                                                                                         //if this item has contents
                {
                    for (int x = item.Contents.Count - 1; x >= 0; x--)                                                             //for each of the contents
                    {
                        List <SavedObject> contentSave = ParseSavedObjectsFromItemList(new object[] { item.Contents[x] }, areaID); //attempt to parse this into a saved object
                        if (contentSave.Count <= 0)                                                                                //if parsing failed
                        {
                            item.Contents.RemoveAt(x);                                                                             //remove this from the contents list
                        }
                    }
                }

                if (item.Type == SavedObject.ObjectType.Container) //if this is a container
                {
                    //containers have no name or ID to validate, so don't involve them

                    SavedObject saved = new SavedObject() //generate a saved object with these settings
                    {
                        Type       = item.Type,
                        ConfigItem = item
                    };
                    Monitor.VerboseLog($"Parsed \"{item.Category}\" as a container type.");
                    return(saved);
                }

                if (item.Type == SavedObject.ObjectType.DGA) //if this is a DGA item
                {
                    if (DGAItemAPI != null)                  //if DGA's API is loaded
                    {
                        try
                        {
                            object testItem = DGAItemAPI.SpawnDGAItem(item.Name); //confirm that this item can be created

                            if (testItem != null)                                 //if this item was created successfully
                            {
                                if (testItem is Item)                             //if the item is an Item or any subclass of it (SDV object, etc)
                                {
                                    SavedObject saved = new SavedObject()         //generate a saved object with these settings
                                    {
                                        Type       = item.Type,
                                        Name       = item.Name,
                                        ConfigItem = item
                                    };
                                    Monitor.VerboseLog($"Parsed \"{item.Name}\" as a DGA item.");
                                    return(saved);
                                }
                                else //if this item not an Item
                                {
                                    Monitor.Log($"An area's item list contains a Dynamic Game Assets (DGA) item of a type that FTM does not recognize.", LogLevel.Info);
                                    Monitor.Log($"Affected spawn area: \"{areaID}\"", LogLevel.Info);
                                    Monitor.Log($"Item name: \"{item.Name}\"", LogLevel.Info);
                                    Monitor.Log($"This may be caused by an error in the item list or a type of custom item that FTM cannot spawn. The affected item will be skipped.", LogLevel.Info);
                                    return(null);
                                }
                            }
                            else
                            {
                                Monitor.Log($"An area's item list contains a Dynamic Game Assets (DGA) item name that does not match any loaded DGA items.", LogLevel.Info);
                                Monitor.Log($"Affected spawn area: \"{areaID}\"", LogLevel.Info);
                                Monitor.Log($"Item name: \"{item.Name}\"", LogLevel.Info);
                                Monitor.Log($"This may be caused by an error in the item list or a modded object that wasn't loaded. The affected item will be skipped.", LogLevel.Info);
                                return(null);
                            }
                        }
                        catch (Exception ex)
                        {
                            Monitor.Log($"An area's item list contains a Dynamic Game Assets (DGA) item, but an error occurred while test-spawning the item.", LogLevel.Info);
                            Monitor.Log($"Affected spawn area: \"{areaID}\"", LogLevel.Info);
                            Monitor.Log($"Item name: \"{item.Name}\"", LogLevel.Info);
                            Monitor.Log($"The affected item will be skipped. The auto-generated error message has been added to the log.", LogLevel.Info);
                            Monitor.Log($"----------", LogLevel.Trace);
                            Monitor.Log($"{ex.ToString()}", LogLevel.Trace);
                            return(null);
                        }
                    }
                    else //if DGA's API is unavailable
                    {
                        Monitor.Log($"An area's item list contains a Dynamic Game Assets (DGA) item, but that mod's interface is unavailable.", LogLevel.Info);
                        Monitor.Log($"Affected spawn area: \"{areaID}\"", LogLevel.Info);
                        Monitor.Log($"Item name: \"{item.Name}\"", LogLevel.Info);
                        Monitor.Log($"If DGA is not installed, please install it. If FTM displayed an error about DGA's interface, please report this to FTM's developer. The affected item will be skipped.", LogLevel.Info);
                        return(null);
                    }
                }

                string savedName = item.Category + ":" + item.Name;

                int?itemID = GetItemID(item.Category, item.Name); //get an item ID for the category and name

                if (itemID.HasValue)                              //if a matching item ID was found
                {
                    SavedObject saved = new SavedObject()         //generate a saved object with these settings
                    {
                        Type       = item.Type,
                        Name       = savedName,
                        ID         = itemID.Value,
                        ConfigItem = item
                    };
                    Monitor.VerboseLog($"Parsed \"{item.Category}\": \"{item.Name}\" into item ID: {itemID}");
                    return(saved);
                }
                else //if no matching item ID was found
                {
                    Monitor.Log($"An area's item list contains a complex item definition that did not match any loaded items.", LogLevel.Info);
                    Monitor.Log($"Affected spawn area: \"{areaID}\"", LogLevel.Info);
                    Monitor.Log($"Item name: \"{savedName}\"", LogLevel.Info);
                    Monitor.Log($"This may be caused by an error in the item list or a modded item that wasn't loaded. The affected item will be skipped.", LogLevel.Info);
                    return(null);
                }
            }