/// <summary>Checks whether a config file should be used with the currently loaded farm.</summary> /// <param name="config">The FarmConfig to be checked.</param> /// <param name="pack">The content pack associated with this file, if any.</param> /// <returns>True if the file should be used with the current farm; false otherwise.</returns> public static bool CheckFileConditions(FarmConfig config, IContentPack pack) { Monitor.Log("Checking file conditions...", LogLevel.Trace); //check farm type if (config.File_Conditions.FarmTypes != null && config.File_Conditions.FarmTypes.Length > 0) { Monitor.Log("Farm type condition(s) found. Checking...", LogLevel.Trace); bool validType = false; foreach (object obj in config.File_Conditions.FarmTypes) //for each listed farm type { int type = -1; //parse the farm type object into an integer (int type) if (obj is long || obj is int) //if the object is a readable integer { type = Convert.ToInt32(obj); //convert it to a 32-bit integer and use it } else if (obj is string name) //if the object is a string, cast it as one { if (name.Equals("All", StringComparison.OrdinalIgnoreCase) || name.Equals("Any", StringComparison.OrdinalIgnoreCase)) //if this is "all" or "any" { validType = true; break; //skip checking the rest of the farm types } else if (Enum.TryParse(name, true, out FarmTypes farmType)) //if this name can be parsed into a FarmTypes enum { type = (int)farmType; //use it as an integer } else //if this is a string, but not a recognized value { if (int.TryParse(name, out int parsed)) //if this string can be parsed as an integer { type = parsed; //use the parsed value } else //if this string cannot be parsed { Monitor.Log($"This setting in the Farm Types list could not be parsed: {name}", LogLevel.Debug); Monitor.Log($"The setting will be ignored. If it's intended to be a custom farm type, please use its ID number instead of its name.", LogLevel.Debug); continue; //skip to the next farm type condition } } } if (type == Game1.whichFarm) //if the parsed type matches the current farm type { validType = true; break; //skip checking the rest of the farm types } else if (Game1.whichFarm == 200) //if this may be a MTN farm type, handle compatibility (based on MTN 2.1.0-beta8) { //if MTN is installed, use reflection to access its "whichFarm" equivalent try { IModInfo modInfo = Helper.ModRegistry.Get("SgtPickles.MTN"); // get MTN info (null if it's not installed) if (modInfo == null) //if MTN isn't installed { continue; //skip to the next farm type check } object mod = modInfo.GetType().GetProperty("Mod", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modInfo); //get MTN's Mod instance if (mod == null) //if it couldn't be accessed { continue; //skip to the next farm type check } object customManager = mod.GetType().GetProperty("CustomManager", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(mod); //get CustomManager instance if (customManager == null) //if it couldn't be accessed { continue; //skip to the next farm type check } object loadedFarm = customManager.GetType().GetProperty("LoadedFarm", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(customManager); //get LoadedFarm instance if (loadedFarm == null) //if it couldn't be accessed { continue; //skip to the next farm type check } int farmID = (int)loadedFarm.GetType().GetProperty("ID", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(loadedFarm); //get the loaded farm's ID if (type == farmID) //if MTN's custom farm ID matches the parsed type { Monitor.VerboseLog($"Farm type matches the loaded MTN farm type's ID: {type}"); validType = true; break; //skip checking the rest of the farm types } } catch (Exception) //if any exception is thrown while accessing MTN { Monitor.Log($"Error encountered while trying to check MTN farm type. This check may be obsolete or require updates.", LogLevel.Trace); continue; //skip to the next farm type check } } } if (validType) //if a valid farm type was listed { Monitor.Log("Farm type matched a setting. File allowed.", LogLevel.Trace); } else { Monitor.Log("Farm type did NOT match any settings. File disabled.", LogLevel.Trace); return(false); //prevent config use } } //check farmer name if (config.File_Conditions.FarmerNames != null && config.File_Conditions.FarmerNames.Length > 0) { Monitor.Log("Farmer name condition(s) found. Checking...", LogLevel.Trace); bool validName = false; foreach (string name in config.File_Conditions.FarmerNames) //for each listed name { if (name.Equals(Game1.player.Name, StringComparison.OrdinalIgnoreCase)) //if the name matches the current player's { validName = true; break; //skip the rest of these checks } } if (validName) //if a valid farmer name was listed { Monitor.Log("Farmer name matched a setting. File allowed.", LogLevel.Trace); } else { Monitor.Log("Farmer name did NOT match any settings. File disabled.", LogLevel.Trace); return(false); //prevent config use } } //check save file names (technically the save folder name) if (config.File_Conditions.SaveFileNames != null && config.File_Conditions.SaveFileNames.Length > 0) { Monitor.Log("Save file name condition(s) found. Checking...", LogLevel.Trace); bool validSave = false; foreach (string saveName in config.File_Conditions.SaveFileNames) //for each listed save name { if (saveName.Equals(Constants.SaveFolderName, StringComparison.OrdinalIgnoreCase)) //if the name matches the current player's save folder name { validSave = true; break; //skip the rest of these checks } } if (validSave) //if a valid save name was listed { Monitor.Log("Save file name matched a setting. File allowed.", LogLevel.Trace); } else { Monitor.Log("Save file name did NOT match any settings. File disabled.", LogLevel.Trace); return(false); //prevent config use } } //check whether other mods exist if (config.File_Conditions.OtherMods != null && config.File_Conditions.OtherMods.Count > 0) { Monitor.Log("Other mod condition(s) found. Checking...", LogLevel.Trace); bool validMods = true; //whether all entries are accurate (true by default, unlike most other settings) foreach (KeyValuePair <string, bool> entry in config.File_Conditions.OtherMods) //for each mod entry in OtherMods { bool validEntry = !(entry.Value); //whether the current entry is accurate (starts false if the mod should exist; starts true if the mod should NOT exist) foreach (IModInfo mod in Helper.ModRegistry.GetAll()) //for each mod currently loaded by SMAPI { if (entry.Value == true) //if the mod should exist { if (entry.Key.Equals(mod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase)) //if this mod's UniqueID matches the OtherMods entry { validEntry = true; //this entry is valid break; //skip the rest of these checks } } else //if the mod should NOT exist { if (entry.Key.Equals(mod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase)) //if this mod's UniqueID matches the OtherMods entry { validEntry = false; //this entry is invalid break; //skip the rest of these checks } } } if (validEntry) //if the current mod entry is valid { Monitor.Log($"Mod check successful: \"{entry.Key}\" {(entry.Value ? "does exist" : "does not exist")}.", LogLevel.Trace); } else //if the current mod entry is NOT valid { Monitor.Log($"Mod check failed: \"{entry.Key}\" {(entry.Value ? "does not exist" : "does exist")}.", LogLevel.Trace); validMods = false; break; //skip the rest of these checks } } if (validMods) //if all mod entries in the list are valid { Monitor.Log("The OtherMods list matches the player's mods. File allowed.", LogLevel.Trace); } else //if any entries were NOT valid { Monitor.Log("The OtherMods list does NOT match the player's mods. File disabled.", LogLevel.Trace); return(false); //prevent config use } } return(true); //all checks were successful; config should be used }
/// <summary>Validates a single instance of farm data, correcting obsolete/invalid settings automatically.</summary> /// <param name="config">The contents of a single config file to be validated.</param> /// <param name="pack">The content pack associated with this config data; null if the file was from this mod's own data folder.</param> public static void ValidateFarmData(FarmConfig config, IContentPack pack) { if (pack != null) { Monitor.Log($"Validating data from content pack: {pack.Manifest.Name}", LogLevel.Trace); } else { Monitor.Log("Validating data from FarmTypeManager/data", LogLevel.Trace); } List <SpawnArea[]> allAreas = new List <SpawnArea[]>(); //a unified list of each "Areas" array in this config file //add each group of spawn areas to the list (unless its config section is null) if (config.Forage_Spawn_Settings != null) { allAreas.Add(config.Forage_Spawn_Settings.Areas); } if (config.Large_Object_Spawn_Settings != null) { allAreas.Add(config.Large_Object_Spawn_Settings.Areas); } if (config.Ore_Spawn_Settings != null) { allAreas.Add(config.Ore_Spawn_Settings.Areas); } if (config.Monster_Spawn_Settings != null) { allAreas.Add(config.Monster_Spawn_Settings.Areas); } Monitor.Log("Checking for duplicate UniqueAreaIDs...", LogLevel.Trace); HashSet <string> IDs = new HashSet <string>(); //a record of all unique IDs encountered during this process //erase any duplicate IDs and record the others in the "IDs" hashset foreach (SpawnArea[] areas in allAreas) //for each "Areas" array in allAreas { foreach (SpawnArea area in areas) //for each area in the current array { if (String.IsNullOrWhiteSpace(area.UniqueAreaID) || area.UniqueAreaID.ToLower() == "null") //if the area ID is null, blank, or the string "null" (to account for user confusion) { continue; //this name will be replaced later, so ignore it for now } if (IDs.Contains(area.UniqueAreaID)) //if this area's ID was already encountered { Monitor.Log($"Duplicate UniqueAreaID found: \"{area.UniqueAreaID}\" will be renamed.", LogLevel.Debug); if (pack != null) //if this config is from a content pack { Monitor.Log($"Content pack: {pack.Manifest.Name}", LogLevel.Info); Monitor.Log($"If this happened after updating another mod, it might cause certain conditions (such as one-time-only spawns) to reset in that area.", LogLevel.Debug); } area.UniqueAreaID = ""; //erase this area's ID, marking it for replacement } else //if this ID is unique so far { IDs.Add(area.UniqueAreaID); //add the area to the ID set } } } Monitor.Log("Assigning new UniqueAreaIDs to any blanks or duplicates...", LogLevel.Trace); string newName; //temp storage for a new ID while it's created/tested int newNumber; //temp storage for the numeric part of a new ID //create new IDs for any empty ones foreach (SpawnArea[] areas in allAreas) //for each "Areas" array in allAreas { foreach (SpawnArea area in areas) //for each area in the current array { if (String.IsNullOrWhiteSpace(area.UniqueAreaID) || area.UniqueAreaID.ToLower() == "null") //if the area ID is null, blank, or the string "null" (to account for user confusion) { //create a new name, based on which type of area this is newName = area.MapName; if (area is ForageSpawnArea) { newName += " forage area "; } else if (area is LargeObjectSpawnArea) { newName += " large object area "; } else if (area is OreSpawnArea) { newName += " ore area "; } else if (area is MonsterSpawnArea) { newName += " monster area "; } else { newName += " area "; } newNumber = 1; while (IDs.Contains(newName + newNumber)) //if this ID wouldn't be unique { newNumber++; //increment and try again } area.UniqueAreaID = newName + newNumber; //apply the new unique ID Monitor.Log($"New UniqueAreaID assigned: {area.UniqueAreaID}", LogLevel.Trace); } IDs.Add(area.UniqueAreaID); //the ID is finalized, so add it to the set of encountered IDs } } //confirm that any paired min/max settings are in the correct order foreach (SpawnArea[] areas in allAreas) //for each "Areas" array in allAreas { foreach (SpawnArea area in areas) //for each area in the current array { if (area.MinimumSpawnsPerDay > area.MaximumSpawnsPerDay) //if the min and max are in the wrong order { //swap min and max int temp = area.MinimumSpawnsPerDay; area.MinimumSpawnsPerDay = area.MaximumSpawnsPerDay; area.MaximumSpawnsPerDay = temp; Monitor.Log($"Swapping minimum and maximum spawns per day for this area: {area.UniqueAreaID}", LogLevel.Trace); } if (area.SpawnTiming.StartTime > area.SpawnTiming.EndTime) //if start and end are in the wrong order { //swap start and end StardewTime temp = area.SpawnTiming.StartTime; area.SpawnTiming.StartTime = area.SpawnTiming.EndTime; area.SpawnTiming.EndTime = temp; Monitor.Log($"Swapping StartTime and EndTime in the SpawnTiming settings for this area: {area.UniqueAreaID}", LogLevel.Trace); } } } //detect invalid sound names and warn the user //NOTE: this will not remove the invalid name, in case the problem is related to custom sound loading foreach (SpawnArea[] areas in allAreas) //for each "Areas" array in allAreas { foreach (SpawnArea area in areas) //for each area in the current array { if (area.SpawnTiming.SpawnSound != null && area.SpawnTiming.SpawnSound.Trim() != "") //if a SpawnSound has been provided for this area { try { Game1.soundBank.GetCue(area.SpawnTiming.SpawnSound); //test whether this sound exists by retrieving it from the game's soundbank } catch //if an exception is thrown while retrieving the sound { Monitor.Log($"This spawn sound could not be found: {area.SpawnTiming.SpawnSound}", LogLevel.Debug); Monitor.Log($"Please make sure the sound's name is spelled and capitalized correctly. Sound names are case-sensitive.", LogLevel.Debug); Monitor.Log($"Area: {area.UniqueAreaID}", LogLevel.Debug); if (pack != null) //if this file is from a content pack { Monitor.Log($"Content pack: {pack.Manifest.Name}", LogLevel.Debug); } else //if this file is from FarmTypeManager/data { Monitor.Log($"File: FarmTypeManager/data/{Constants.SaveFolderName}.json", LogLevel.Debug); } } } } } if (pack != null) { Monitor.Log($"Validation complete for content pack: {pack.Manifest.Name}", LogLevel.Trace); } else { Monitor.Log("Validation complete for data from FarmTypeManager/data", LogLevel.Trace); } return; }
/// <summary>Loads all available data files for the current farm into FarmDataList. Checks the mod's data folder and any relevant content packs.</summary> /// <param name="helper">SMAPI interface, used here to access files.</param> /// <returns>True if the files loaded successfully; false otherwise.</returns> public static void LoadFarmData(IModHelper helper) { Monitor.Log("Beginning file loading process...", LogLevel.Trace); //clear any existing farm data FarmDataList = new List <FarmData>(); FarmConfig config; //temp for the current config as it's loaded InternalSaveData save; //temp for the current save as it's loaded if (MConfig.EnableContentPacks) //if content packs are enabled { //load data from each relevant content pack foreach (IContentPack pack in helper.ContentPacks.GetOwned()) { Monitor.Log($"Loading files from content pack: {pack.Manifest.Name}", LogLevel.Trace); //clear each temp object config = null; save = null; //attempt to load the farm config from this pack try { config = pack.ReadJsonFile <FarmConfig>($"content.json"); //load the content pack's farm config (null if it doesn't exist) } catch (Exception ex) { Monitor.Log($"Warning: This content pack could not be parsed correctly: {pack.Manifest.Name}", LogLevel.Warn); Monitor.Log($"Please edit the content.json file or reinstall the content pack. The auto-generated error message is displayed below:", LogLevel.Warn); Monitor.Log($"----------", LogLevel.Warn); Monitor.Log($"{ex.Message}", LogLevel.Warn); continue; //skip to the next content pack } if (config == null) //no config file found for this farm { Monitor.Log($"Warning: The content.json file for this content pack could not be found: {pack.Manifest.Name}", LogLevel.Warn); Monitor.Log($"Please reinstall the content pack. If you are its author, please create a config file named content.json in the pack's main folder (not the /data/ folder).", LogLevel.Warn); continue; //skip to the next content pack } //attempt to load the save data for this pack and specific farm try { save = pack.ReadJsonFile <InternalSaveData>($"data/{Constants.SaveFolderName}_SaveData.save"); //load the content pack's save data for this farm (null if it doesn't exist) } catch (Exception ex) { Monitor.Log($"Warning: Your farm's save data for this content pack could not be parsed correctly: {pack.Manifest.Name}", LogLevel.Warn); Monitor.Log($"This file will need to be edited or deleted: data/{Constants.SaveFolderName}_SaveData.save", LogLevel.Warn); Monitor.Log($"The content pack will be skipped until this issue is fixed. The auto-generated error message is displayed below:", LogLevel.Warn); Monitor.Log($"----------", LogLevel.Warn); Monitor.Log($"{ex.Message}", LogLevel.Warn); continue; //skip to the next content pack } if (save == null) //no save file found for this farm { save = new InternalSaveData(); //load the (built-in) default save settings } ValidateFarmData(config, pack); //validate certain data in the current file before using it pack.WriteJsonFile($"content.json", config); //update the content pack's config file pack.WriteJsonFile(Path.Combine("data", $"{Constants.SaveFolderName}_SaveData.save"), save); //create or update the content pack's save file for the current farm if (CheckFileConditions(config, pack, helper)) //check file conditions; only use the current data if this returns true { FarmDataList.Add(new FarmData(config, save, pack)); //add the config, save, and content pack to the farm data list Monitor.Log("Content pack loaded successfully.", LogLevel.Trace); } } Monitor.Log("All available content packs checked.", LogLevel.Trace); } else { Monitor.Log("Content packs disabled in config.json. Skipping to local files...", LogLevel.Trace); } //clear each temp object config = null; save = null; Monitor.Log("Loading files from FarmTypeManager/data", LogLevel.Trace); //attempt to load the farm config from this mod's data folder //NOTE: this should always be done *after* content packs, because it will end the loading process if an error occurs try { config = helper.Data.ReadJsonFile <FarmConfig>(Path.Combine("data", $"{Constants.SaveFolderName}.json")); //load the current save's config file (null if it doesn't exist) } catch (Exception ex) { Monitor.Log($"Warning: This file could not be parsed correctly: FarmTypeManager/data/{Constants.SaveFolderName}.json", LogLevel.Warn); Monitor.Log($"Please edit the file, or delete it and reload your farm to generate a new config file.", LogLevel.Warn); Monitor.Log($"Your config file will be skipped until this issue is fixed. The auto-generated error message is displayed below:", LogLevel.Warn); Monitor.Log($"----------", LogLevel.Warn); Monitor.Log($"{ex.Message}", LogLevel.Warn); return; //end this process without adding this set of farm data } if (config == null) //no config file found for this farm { //attempt to load the default.json config file try { config = helper.Data.ReadJsonFile <FarmConfig>(Path.Combine("data", "default.json")); //load the default.json config file (null if it doesn't exist) } catch (Exception ex) { Monitor.Log($"Warning: This file could not be parsed correctly: FarmTypeManager/data/default.json", LogLevel.Warn); Monitor.Log($"Please edit the file, or delete it and reload your farm to generate a new default config file.", LogLevel.Warn); Monitor.Log($"Your config file will be skipped until this issue is fixed. The auto-generated error message is displayed below:", LogLevel.Warn); Monitor.Log($"----------", LogLevel.Warn); Monitor.Log($"{ex.Message}", LogLevel.Warn); return; //end this process without adding this set of farm data } if (config == null) //no default.json config file { config = new FarmConfig(); //load the (built-in) default config settings } ValidateFarmData(config, null); //validate certain data in the current file before using it helper.Data.WriteJsonFile(Path.Combine("data", "default.json"), config); //create or update the default.json config file } //attempt to load the save data for this farm try { save = helper.Data.ReadJsonFile <InternalSaveData>(Path.Combine("data", $"{Constants.SaveFolderName}_SaveData.save")); //load the mod's save data for this farm (null if it doesn't exist) } catch (Exception ex) { Monitor.Log($"Warning: This file could not be parsed correctly: FarmTypeManager/data/{Constants.SaveFolderName}_SaveData.save", LogLevel.Warn); Monitor.Log($"Please edit the file, or delete it and reload your farm to generate a new savedata file.", LogLevel.Warn); Monitor.Log($"Your config file will be skipped until this issue is fixed. The auto-generated error message is displayed below:", LogLevel.Warn); Monitor.Log($"----------", LogLevel.Warn); Monitor.Log($"{ex.Message}", LogLevel.Warn); return; //end this process without adding this set of farm data } if (save == null) //no save file found for this farm { save = new InternalSaveData(); //load the (built-in) default save settings } ValidateFarmData(config, null); //validate certain data in the current file before using it helper.Data.WriteJsonFile(Path.Combine("data", $"{Constants.SaveFolderName}.json"), config); //create or update the config file for the current farm helper.Data.WriteJsonFile(Path.Combine("data", $"{Constants.SaveFolderName}_SaveData.save"), save); //create or update this config's save file for the current farm if (CheckFileConditions(config, null, helper)) //check file conditions; only use the current data if this returns true { FarmDataList.Add(new FarmData(config, save, null)); //add the config, save, and a *null* content pack to the farm data list Monitor.Log("FarmTypeManager/data farm data loaded successfully.", LogLevel.Trace); } }
} //NOTE: this should be null when no content pack was involved, i.e. the data came from files in the mod's own folder public FarmData(FarmConfig config, InternalSaveData save, IContentPack pack) { Config = config; Save = save; Pack = pack; }
/// <summary>Checks whether a config file should be used with the currently loaded farm.</summary> /// <param name="config">The FarmConfig to be checked.</param> /// <returns>True if the file should be used with the current farm; false otherwise.</returns> public static bool CheckFileConditions(FarmConfig config, IContentPack pack, IModHelper helper) { Monitor.Log("Checking file conditions...", LogLevel.Trace); //check "reset main data folder" flag //NOTE: it's preferable to do this as the first step; it's intended to be a one-off cleaning process, rather than a conditional effect if (config.File_Conditions.ResetMainDataFolder && MConfig.EnableContentPackFileChanges) //if "reset" is true and file changes are enabled { if (pack != null) //if this is part of a content pack { //attempt to load the content pack's global save data ContentPackSaveData packSave = null; try { packSave = pack.ReadJsonFile <ContentPackSaveData>(Path.Combine("data", "ContentPackSaveData.save")); //load the content pack's global save data (null if it doesn't exist) } catch (Exception ex) { Monitor.Log($"Warning: This content pack's save data could not be parsed correctly: {pack.Manifest.Name}", LogLevel.Warn); Monitor.Log($"Affected file: data/ContentPackSaveData.save", LogLevel.Warn); Monitor.Log($"Please delete the file and/or contact the mod's developer.", LogLevel.Warn); Monitor.Log($"The content pack will be skipped until this issue is fixed. The auto-generated error message is displayed below:", LogLevel.Warn); Monitor.Log($"----------", LogLevel.Warn); Monitor.Log($"{ex.Message}", LogLevel.Warn); return(false); //disable this content pack's config, since it may require this process to function } if (packSave == null) //no global save data exists for this content pack { packSave = new ContentPackSaveData(); } if (!packSave.MainDataFolderReset) //if this content pack has NOT reset the main data folder yet { Monitor.Log($"ResetMainDataFolder requested by content pack: {pack.Manifest.Name}", LogLevel.Debug); string dataPath = Path.Combine(helper.DirectoryPath, "data"); //the path to this mod's data folder DirectoryInfo dataFolder = new DirectoryInfo(dataPath); //an object representing this mod's data directory if (dataFolder.Exists) //the data folder exists { Monitor.Log("Attempting to archive data folder...", LogLevel.Trace); try { string archivePath = Path.Combine(helper.DirectoryPath, "data", "archive", DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss")); DirectoryInfo archiveFolder = Directory.CreateDirectory(archivePath); //create a timestamped archive folder foreach (FileInfo file in dataFolder.GetFiles()) //for each file in dataFolder { file.MoveTo(Path.Combine(archiveFolder.FullName, file.Name)); //move each file to archiveFolder } } catch (Exception ex) { Monitor.Log($"Warning: This content pack attempted to archive Farm Type Manager's data folder but failed: {pack.Manifest.Name}", LogLevel.Warn); Monitor.Log($"Please report this issue to Farm Type Manager's developer. This might also be fixed by manually removing your FarmTypeManager/data/ files.", LogLevel.Warn); Monitor.Log($"The content pack will be skipped until this issue is fixed. The auto-generated error message is displayed below:", LogLevel.Warn); Monitor.Log($"----------", LogLevel.Warn); Monitor.Log($"{ex.Message}", LogLevel.Warn); return(false); //disable this content pack's config, since it may require this process to function } } else //the data folder doesn't exist { Monitor.Log("Data folder not found; assuming it was deleted or not yet generated.", LogLevel.Trace); } packSave.MainDataFolderReset = true; //update save data } pack.WriteJsonFile(Path.Combine("data", "ContentPackSaveData.save"), packSave); //update the content pack's global save data file Monitor.Log("Data folder archive successful.", LogLevel.Trace); } else //if this is NOT part of a content pack { Monitor.Log("This farm's config file has ResetMainDataFolder = true, but this setting only works for content packs.", LogLevel.Info); } } //check farm type if (config.File_Conditions.FarmTypes != null && config.File_Conditions.FarmTypes.Length > 0) { Monitor.Log("Farm type condition(s) found. Checking...", LogLevel.Trace); bool validType = false; foreach (string type in config.File_Conditions.FarmTypes) //for each listed farm type { if (type.Equals("All", StringComparison.OrdinalIgnoreCase) || type.Equals("Any", StringComparison.OrdinalIgnoreCase)) //if "all" or "any" is listed { validType = true; break; //skip the rest of these checks } switch (Game1.whichFarm) //compare to the current farm type { case (int)Utility.FarmTypes.Standard: if (type.Equals("Standard", StringComparison.OrdinalIgnoreCase) || type.Equals("Default", StringComparison.OrdinalIgnoreCase) || type.Equals("Normal", StringComparison.OrdinalIgnoreCase)) { validType = true; } break; case (int)Utility.FarmTypes.Riverland: if (type.Equals("Riverland", StringComparison.OrdinalIgnoreCase) || type.Equals("Fishing", StringComparison.OrdinalIgnoreCase) || type.Equals("Fish", StringComparison.OrdinalIgnoreCase)) { validType = true; } break; case (int)Utility.FarmTypes.Forest: if (type.Equals("Forest", StringComparison.OrdinalIgnoreCase) || type.Equals("Foraging", StringComparison.OrdinalIgnoreCase) || type.Equals("Forage", StringComparison.OrdinalIgnoreCase) || type.Equals("Woodland", StringComparison.OrdinalIgnoreCase)) { validType = true; } break; case (int)Utility.FarmTypes.Hilltop: if (type.Equals("Hill-top", StringComparison.OrdinalIgnoreCase) || type.Equals("Hilltop", StringComparison.OrdinalIgnoreCase) || type.Equals("Mining", StringComparison.OrdinalIgnoreCase) || type.Equals("Mine", StringComparison.OrdinalIgnoreCase)) { validType = true; } break; case (int)Utility.FarmTypes.Wilderness: if (type.Equals("Wilderness", StringComparison.OrdinalIgnoreCase) || type.Equals("Combat", StringComparison.OrdinalIgnoreCase) || type.Equals("Monster", StringComparison.OrdinalIgnoreCase)) { validType = true; } break; } if (validType) //if a valid weather condition was listed { break; //skip the rest of these checks } } if (validType) //if a valid farm type was listed { Monitor.Log("Farm type matched a setting. File allowed.", LogLevel.Trace); } else { Monitor.Log("Farm type did NOT match any settings. File disabled.", LogLevel.Trace); return(false); //prevent config use } } //check farmer name if (config.File_Conditions.FarmerNames != null && config.File_Conditions.FarmerNames.Length > 0) { Monitor.Log("Farmer name condition(s) found. Checking...", LogLevel.Trace); bool validName = false; foreach (string name in config.File_Conditions.FarmerNames) //for each listed name { if (name.Equals(Game1.player.Name, StringComparison.OrdinalIgnoreCase)) //if the name matches the current player's { validName = true; break; //skip the rest of these checks } } if (validName) //if a valid farmer name was listed { Monitor.Log("Farmer name matched a setting. File allowed.", LogLevel.Trace); } else { Monitor.Log("Farmer name did NOT match any settings. File disabled.", LogLevel.Trace); return(false); //prevent config use } } //check save file names (technically the save folder name) if (config.File_Conditions.SaveFileNames != null && config.File_Conditions.SaveFileNames.Length > 0) { Monitor.Log("Save file name condition(s) found. Checking...", LogLevel.Trace); bool validSave = false; foreach (string saveName in config.File_Conditions.SaveFileNames) //for each listed save name { if (saveName.Equals(Constants.SaveFolderName, StringComparison.OrdinalIgnoreCase)) //if the name matches the current player's save folder name { validSave = true; break; //skip the rest of these checks } } if (validSave) //if a valid save name was listed { Monitor.Log("Save file name matched a setting. File allowed.", LogLevel.Trace); } else { Monitor.Log("Save file name did NOT match any settings. File disabled.", LogLevel.Trace); return(false); //prevent config use } } //check whether other mods exist if (config.File_Conditions.OtherMods != null && config.File_Conditions.OtherMods.Count > 0) { Monitor.Log("Other mod condition(s) found. Checking...", LogLevel.Trace); bool validMods = true; //whether all entries are accurate (true by default, unlike most other settings) foreach (KeyValuePair <string, bool> entry in config.File_Conditions.OtherMods) //for each mod entry in OtherMods { bool validEntry = !(entry.Value); //whether the current entry is accurate (starts false if the mod should exist; starts true if the mod should NOT exist) foreach (IModInfo mod in helper.ModRegistry.GetAll()) //for each mod currently loaded by SMAPI { if (entry.Value == true) //if the mod should exist { if (entry.Key.Equals(mod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase)) //if this mod's UniqueID matches the OtherMods entry { validEntry = true; //this entry is valid break; //skip the rest of these checks } } else //if the mod should NOT exist { if (entry.Key.Equals(mod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase)) //if this mod's UniqueID matches the OtherMods entry { validEntry = false; //this entry is invalid break; //skip the rest of these checks } } } if (validEntry) //if the current mod entry is valid { Monitor.Log($"Mod check successful: \"{entry.Key}\" {(entry.Value ? "does exist" : "does not exist")}.", LogLevel.Trace); } else //if the current mod entry is NOT valid { Monitor.Log($"Mod check failed: \"{entry.Key}\" {(entry.Value ? "does not exist" : "does exist")}.", LogLevel.Trace); validMods = false; break; //skip the rest of these checks } } if (validMods) //if all mod entries in the list are valid { Monitor.Log("The OtherMods list matches the player's mods. File allowed.", LogLevel.Trace); } else //if any entries were NOT valid { Monitor.Log("The OtherMods list does NOT match the player's mods. File disabled.", LogLevel.Trace); return(false); //prevent config use } } return(true); //all checks were successful; config should be used }
/// <summary>Checks whether a config file should be used with the currently loaded farm.</summary> /// <param name="config">The FarmConfig to be checked.</param> /// <param name="pack">The content pack associated with this file, if any.</param> /// <returns>True if the file should be used with the current farm; false otherwise.</returns> public static bool CheckFileConditions(FarmConfig config, IContentPack pack) { Monitor.Log("Checking file conditions...", LogLevel.Trace); //check "reset main data folder" flag //NOTE: it's preferable to do this as the first step; it's intended to be a one-off cleaning process, rather than a conditional effect if (config.File_Conditions.ResetMainDataFolder && MConfig.EnableContentPackFileChanges) //if "reset" is true and file changes are enabled { if (pack != null) //if this is part of a content pack { //attempt to load the content pack's global save data ContentPackSaveData packSave = null; try { packSave = pack.ReadJsonFile <ContentPackSaveData>(Path.Combine("data", "ContentPackSaveData.save")); //load the content pack's global save data (null if it doesn't exist) } catch (Exception ex) { Monitor.Log($"Warning: This content pack's save data could not be parsed correctly: {pack.Manifest.Name}", LogLevel.Warn); Monitor.Log($"Affected file: data/ContentPackSaveData.save", LogLevel.Warn); Monitor.Log($"Please delete the file and/or contact the mod's developer.", LogLevel.Warn); Monitor.Log($"The content pack will be skipped until this issue is fixed. The auto-generated error message is displayed below:", LogLevel.Warn); Monitor.Log($"----------", LogLevel.Warn); Monitor.Log($"{ex.Message}", LogLevel.Warn); return(false); //disable this content pack's config, since it may require this process to function } if (packSave == null) //no global save data exists for this content pack { packSave = new ContentPackSaveData(); } if (!packSave.MainDataFolderReset) //if this content pack has NOT reset the main data folder yet { Monitor.Log($"ResetMainDataFolder requested by content pack: {pack.Manifest.Name}", LogLevel.Debug); string dataPath = Path.Combine(Helper.DirectoryPath, "data"); //the path to this mod's data folder DirectoryInfo dataFolder = new DirectoryInfo(dataPath); //an object representing this mod's data directory if (dataFolder.Exists) //the data folder exists { Monitor.Log("Attempting to archive data folder...", LogLevel.Trace); try { string archivePath = Path.Combine(Helper.DirectoryPath, "data", "archive", DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss")); DirectoryInfo archiveFolder = Directory.CreateDirectory(archivePath); //create a timestamped archive folder foreach (FileInfo file in dataFolder.GetFiles()) //for each file in dataFolder { file.MoveTo(Path.Combine(archiveFolder.FullName, file.Name)); //move each file to archiveFolder } } catch (Exception ex) { Monitor.Log($"Warning: This content pack attempted to archive Farm Type Manager's data folder but failed: {pack.Manifest.Name}", LogLevel.Warn); Monitor.Log($"Please report this issue to Farm Type Manager's developer. This might also be fixed by manually removing your FarmTypeManager/data/ files.", LogLevel.Warn); Monitor.Log($"The content pack will be skipped until this issue is fixed. The auto-generated error message is displayed below:", LogLevel.Warn); Monitor.Log($"----------", LogLevel.Warn); Monitor.Log($"{ex.Message}", LogLevel.Warn); return(false); //disable this content pack's config, since it may require this process to function } } else //the data folder doesn't exist { Monitor.Log("Data folder not found; assuming it was deleted or not yet generated.", LogLevel.Trace); } packSave.MainDataFolderReset = true; //update save data } pack.WriteJsonFile(Path.Combine("data", "ContentPackSaveData.save"), packSave); //update the content pack's global save data file Monitor.Log("Data folder archive successful.", LogLevel.Trace); } else //if this is NOT part of a content pack { Monitor.Log("This farm's config file has ResetMainDataFolder = true, but this setting only works for content packs.", LogLevel.Info); } } //check farm type if (config.File_Conditions.FarmTypes != null && config.File_Conditions.FarmTypes.Length > 0) { Monitor.Log("Farm type condition(s) found. Checking...", LogLevel.Trace); bool validType = false; foreach (object obj in config.File_Conditions.FarmTypes) //for each listed farm type { int type = -1; //parse the farm type object into an integer (int type) if (obj is long || obj is int) //if the object is a readable integer { type = Convert.ToInt32(obj); //convert it to a 32-bit integer and use it } else if (obj is string name) //if the object is a string, cast it as one { if (name.Equals("All", StringComparison.OrdinalIgnoreCase) || name.Equals("Any", StringComparison.OrdinalIgnoreCase)) //if this is "all" or "any" { validType = true; break; //skip checking the rest of the farm types } else if (Enum.TryParse(name, true, out FarmTypes farmType)) //if this name can be parsed into a FarmTypes enum { type = (int)farmType; //use it as an integer } else //if this is a string, but not a recognized value { if (int.TryParse(name, out int parsed)) //if this string can be parsed as an integer { type = parsed; //use the parsed value } else //if this string cannot be parsed { Monitor.Log($"This setting in the Farm Types list could not be parsed: {name}", LogLevel.Debug); Monitor.Log($"The setting will be ignored. If it's intended to be a custom farm type, please use its ID number instead of its name.", LogLevel.Debug); continue; //skip to the next farm type condition } } } if (type == Game1.whichFarm) //if the parsed type matches the current farm type { validType = true; break; //skip checking the rest of the farm types } else if (Game1.whichFarm == 200) //if this may be a MTN farm type, handle compatibility (based on MTN 2.1.0-beta8) { //if MTN is installed, use reflection to access its "whichFarm" equivalent try { IModInfo modInfo = Helper.ModRegistry.Get("SgtPickles.MTN"); // get MTN info (null if it's not installed) if (modInfo == null) //if MTN isn't installed { continue; //skip to the next farm type check } object mod = modInfo.GetType().GetProperty("Mod", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modInfo); //get MTN's Mod instance if (mod == null) //if it couldn't be accessed { continue; //skip to the next farm type check } object customManager = mod.GetType().GetProperty("CustomManager", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(mod); //get CustomManager instance if (customManager == null) //if it couldn't be accessed { continue; //skip to the next farm type check } object loadedFarm = customManager.GetType().GetProperty("LoadedFarm", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(customManager); //get LoadedFarm instance if (loadedFarm == null) //if it couldn't be accessed { continue; //skip to the next farm type check } int farmID = (int)loadedFarm.GetType().GetProperty("ID", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(loadedFarm); //get the loaded farm's ID if (type == farmID) //if MTN's custom farm ID matches the parsed type { Monitor.VerboseLog($"Farm type matches the loaded MTN farm type's ID: {type}"); validType = true; break; //skip checking the rest of the farm types } } catch (Exception) //if any exception is thrown while accessing MTN { Monitor.Log($"Error encountered while trying to check MTN farm type. This check may be obsolete or require updates.", LogLevel.Trace); continue; //skip to the next farm type check } } } if (validType) //if a valid farm type was listed { Monitor.Log("Farm type matched a setting. File allowed.", LogLevel.Trace); } else { Monitor.Log("Farm type did NOT match any settings. File disabled.", LogLevel.Trace); return(false); //prevent config use } } //check farmer name if (config.File_Conditions.FarmerNames != null && config.File_Conditions.FarmerNames.Length > 0) { Monitor.Log("Farmer name condition(s) found. Checking...", LogLevel.Trace); bool validName = false; foreach (string name in config.File_Conditions.FarmerNames) //for each listed name { if (name.Equals(Game1.player.Name, StringComparison.OrdinalIgnoreCase)) //if the name matches the current player's { validName = true; break; //skip the rest of these checks } } if (validName) //if a valid farmer name was listed { Monitor.Log("Farmer name matched a setting. File allowed.", LogLevel.Trace); } else { Monitor.Log("Farmer name did NOT match any settings. File disabled.", LogLevel.Trace); return(false); //prevent config use } } //check save file names (technically the save folder name) if (config.File_Conditions.SaveFileNames != null && config.File_Conditions.SaveFileNames.Length > 0) { Monitor.Log("Save file name condition(s) found. Checking...", LogLevel.Trace); bool validSave = false; foreach (string saveName in config.File_Conditions.SaveFileNames) //for each listed save name { if (saveName.Equals(Constants.SaveFolderName, StringComparison.OrdinalIgnoreCase)) //if the name matches the current player's save folder name { validSave = true; break; //skip the rest of these checks } } if (validSave) //if a valid save name was listed { Monitor.Log("Save file name matched a setting. File allowed.", LogLevel.Trace); } else { Monitor.Log("Save file name did NOT match any settings. File disabled.", LogLevel.Trace); return(false); //prevent config use } } //check whether other mods exist if (config.File_Conditions.OtherMods != null && config.File_Conditions.OtherMods.Count > 0) { Monitor.Log("Other mod condition(s) found. Checking...", LogLevel.Trace); bool validMods = true; //whether all entries are accurate (true by default, unlike most other settings) foreach (KeyValuePair <string, bool> entry in config.File_Conditions.OtherMods) //for each mod entry in OtherMods { bool validEntry = !(entry.Value); //whether the current entry is accurate (starts false if the mod should exist; starts true if the mod should NOT exist) foreach (IModInfo mod in Helper.ModRegistry.GetAll()) //for each mod currently loaded by SMAPI { if (entry.Value == true) //if the mod should exist { if (entry.Key.Equals(mod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase)) //if this mod's UniqueID matches the OtherMods entry { validEntry = true; //this entry is valid break; //skip the rest of these checks } } else //if the mod should NOT exist { if (entry.Key.Equals(mod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase)) //if this mod's UniqueID matches the OtherMods entry { validEntry = false; //this entry is invalid break; //skip the rest of these checks } } } if (validEntry) //if the current mod entry is valid { Monitor.Log($"Mod check successful: \"{entry.Key}\" {(entry.Value ? "does exist" : "does not exist")}.", LogLevel.Trace); } else //if the current mod entry is NOT valid { Monitor.Log($"Mod check failed: \"{entry.Key}\" {(entry.Value ? "does not exist" : "does exist")}.", LogLevel.Trace); validMods = false; break; //skip the rest of these checks } } if (validMods) //if all mod entries in the list are valid { Monitor.Log("The OtherMods list matches the player's mods. File allowed.", LogLevel.Trace); } else //if any entries were NOT valid { Monitor.Log("The OtherMods list does NOT match the player's mods. File disabled.", LogLevel.Trace); return(false); //prevent config use } } return(true); //all checks were successful; config should be used }
/// <summary>Validates a single instance of farm data, correcting obsolete/invalid settings automatically.</summary> /// <param name="config">The contents of a single config file to be validated.</param> /// <param name="pack">The content pack associated with this config data; null if the file was from this mod's own folders.</param> public static void ValidateFarmData(FarmConfig config, IContentPack pack) { if (pack != null) { Monitor.Log($"Validating data from content pack: {pack.Manifest.Name}", LogLevel.Trace); } else { Monitor.Log("Validating data from FarmTypeManager/data", LogLevel.Trace); } List <SpawnArea[]> allAreas = new List <SpawnArea[]>(); //a unified list of each "Areas" array in this config file allAreas.Add(config.Forage_Spawn_Settings.Areas); allAreas.Add(config.Large_Object_Spawn_Settings.Areas); allAreas.Add(config.Ore_Spawn_Settings.Areas); Monitor.Log("Checking for duplicate UniqueAreaIDs...", LogLevel.Trace); HashSet <string> IDs = new HashSet <string>(); //a record of all unique IDs encountered during this process //erase any duplicate IDs and record the others in the "IDs" hashset foreach (SpawnArea[] areas in allAreas) //for each "Areas" array in allAreas { foreach (SpawnArea area in areas) //for each area in the current array { if (String.IsNullOrWhiteSpace(area.UniqueAreaID) || area.UniqueAreaID.ToLower() == "null") //if the area ID is null, blank, or the string "null" (to account for user confusion) { continue; //this name will be replaced later, so ignore it for now } if (IDs.Contains(area.UniqueAreaID)) //if this area's ID was already encountered { Monitor.Log($"Duplicate UniqueAreaID found: \"{area.UniqueAreaID}\" will be renamed.", LogLevel.Info); if (pack != null) //if this config is from a content pack { Monitor.Log($"Content pack: {pack.Manifest.Name}", LogLevel.Info); Monitor.Log($"If this happens after updating another mod, it might cause certain conditions (such as one-time-only spawns) to reset in that area.", LogLevel.Info); } area.UniqueAreaID = ""; //erase this area's ID, marking it for replacement } else //if this ID is unique so far { IDs.Add(area.UniqueAreaID); //add the area to the ID set } } } Monitor.Log("Assigning new UniqueAreaIDs to any blanks or duplicates...", LogLevel.Trace); string newName; //temp storage for a new ID while it's created/tested int newNumber; //temp storage for the numeric part of a new ID //create new IDs for any empty ones foreach (SpawnArea[] areas in allAreas) //for each "Areas" array in allAreas { foreach (SpawnArea area in areas) //for each area in the current array { if (String.IsNullOrWhiteSpace(area.UniqueAreaID) || area.UniqueAreaID.ToLower() == "null") //if the area ID is null, blank, or the string "null" (to account for user confusion) { //create a new name, based on which type of area this is newName = area.MapName; if (area is ForageSpawnArea) { newName += " forage area "; } else if (area is LargeObjectSpawnArea) { newName += " large object area "; } else if (area is OreSpawnArea) { newName += " ore area "; } else { newName += " area "; } newNumber = 1; while (IDs.Contains(newName + newNumber)) //if this ID wouldn't be unique { newNumber++; //increment and try again } area.UniqueAreaID = newName + newNumber; //apply the new unique ID Monitor.Log($"New UniqueAreaID assigned: {area.UniqueAreaID}", LogLevel.Trace); } IDs.Add(area.UniqueAreaID); //the ID is finalized, so add it to the set of encountered IDs } } if (pack != null) { Monitor.Log($"Validation complete for content pack: {pack.Manifest.Name}", LogLevel.Trace); } else { Monitor.Log("Validation complete for this file from FarmTypeManager/data", LogLevel.Trace); } return; }