/// <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); } }
/// <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); }
/// <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 } } }
/// <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); } }