/// <summary> /// Returns the filepath of the mod assembly. /// </summary> /// <returns>Mod assembly filepath</returns> private string GetAssemblyPath() { // Get list of currently active plugins. IEnumerable <PluginManager.PluginInfo> plugins = PluginManager.instance.GetPluginsInfo(); // Iterate through list. foreach (PluginManager.PluginInfo plugin in plugins) { try { // Get all (if any) mod instances from this plugin. IUserMod[] mods = plugin.GetInstances <IUserMod>(); // Check to see if the primary instance is this mod. if (mods.FirstOrDefault() is PloppableRICOMod) { // Found it! Return path. return(plugin.modPath); } } catch { // Don't care. } } // If we got here, then we didn't find the assembly. Debugging.Message("assembly path not found"); throw new FileNotFoundException(PloppableRICOMod.ModName + ": assembly path not found!"); }
/// <summary> /// Attaches an event hook to options panel visibility, to create/destroy our options panel as appropriate. /// Destroying when not visible saves UI overhead and performance impacts, especially with so many UITextFields. /// </summary> public static void OptionsEventHook() { // Get options panel instance. gameOptionsPanel = UIView.library.Get <UIPanel>("OptionsPanel"); if (gameOptionsPanel == null) { Debugging.Message("couldn't find OptionsPanel"); } else { // Simple event hook to create/destroy GameObject based on appropriate visibility. gameOptionsPanel.eventVisibilityChanged += (control, isVisible) => { // Create/destroy based on visible. if (isVisible) { Create(); } else { Close(); } }; // Recreate panel on system locale change. LocaleManager.eventLocaleChanged += LocaleChanged; } }
/// <summary> /// Saves the current RICO settings to file. /// </summary> private void Save() { // Read current settings from UI elements and convert to XML. SettingsPanel.Panel.Save(); // If the local settings file doesn't already exist, create a new blank template. if (!File.Exists("LocalRICOSettings.xml")) { var newLocalSettings = new PloppableRICODefinition(); var xmlSerializer = new XmlSerializer(typeof(PloppableRICODefinition)); // Create blank file template. using (XmlWriter writer = XmlWriter.Create("LocalRICOSettings.xml")) { xmlSerializer.Serialize(writer, newLocalSettings); } } // Check that file exists before continuing (it really should at this point, but just in case). if (File.Exists("LocalRICOSettings.xml")) { PloppableRICODefinition oldLocalSettings; PloppableRICODefinition newLocalSettings = new PloppableRICODefinition(); XmlSerializer xmlSerializer = new XmlSerializer(typeof(PloppableRICODefinition)); // Read existing file. using (StreamReader streamReader = new StreamReader("LocalRICOSettings.xml")) { oldLocalSettings = xmlSerializer.Deserialize(streamReader) as PloppableRICODefinition; } // Loop though all buildings in the existing file. If they aren't the current selection, write them back to the replacement file. foreach (var buildingDef in oldLocalSettings.Buildings) { if (buildingDef.name != currentSelection.name) { newLocalSettings.Buildings.Add(buildingDef); } } // If current selection has local settings, add them to the replacement file. if (currentSelection.hasLocal) { newLocalSettings.Buildings.Add(currentSelection.local); } // Write replacement file to disk. using (TextWriter writer = new StreamWriter("LocalRICOSettings.xml")) { xmlSerializer.Serialize(writer, newLocalSettings); } } else { Debugging.Message("couldn't find local settings file to save"); } // Force an update of all panels with current values. SettingsPanel.Panel.UpdateSelectedBuilding(currentSelection); }
/// <summary> /// Load settings from XML file. /// </summary> internal static void LoadSettings() { try { // Check to see if configuration file exists. if (File.Exists(SettingsFileName)) { // Read it. using (StreamReader reader = new StreamReader(SettingsFileName)) { XmlSerializer xmlSerializer = new XmlSerializer(typeof(XMLSettingsFile)); if (!(xmlSerializer.Deserialize(reader) is XMLSettingsFile xmlSettingsFile)) { Debugging.Message("couldn't deserialize settings file"); } } } else { Debugging.Message("no settings file found"); } } catch (Exception e) { Debugging.Message("exception reading XML settings file"); Debugging.LogException(e); } }
/// <summary> /// Checks to see if another mod is installed, based on a provided assembly name. /// </summary> /// <param name="assemblyName">Name of the mod assembly</param> /// <param name="enabledOnly">True if the mod needs to be enabled for the purposes of this check; false if it doesn't matter</param> /// <returns>True if the mod is installed (and, if enabledOnly is true, is also enabled), false otherwise</returns> internal static bool IsModInstalled(string assemblyName, bool enabledOnly = false) { // Convert assembly name to lower case. string assemblyNameLower = assemblyName.ToLower(); // Iterate through the full list of plugins. foreach (PluginManager.PluginInfo plugin in PluginManager.instance.GetPluginsInfo()) { foreach (Assembly assembly in plugin.GetAssemblies()) { if (assembly.GetName().Name.ToLower().Equals(assemblyNameLower)) { Debugging.Message("found mod assembly " + assemblyName); if (enabledOnly) { return(plugin.isEnabled); } else { return(true); } } } } // If we've made it here, then we haven't found a matching assembly. return(false); }
/// <summary> /// Saves the current RICO settings to file and then applies them live in-game. /// </summary> private void SaveAndApply() { // Find current prefab instance. BuildingData currentBuildingData = Loading.xmlManager.prefabHash[currentSelection.prefab]; // Save first. Save(); // If we're converting a residential building to something else, then we first should clear out all households. if (currentBuildingData.prefab.GetService() == ItemClass.Service.Residential && !IsCurrentResidential()) { // removeAll argument to true to remove all households. UpdateHouseholds(currentBuildingData.prefab.name, removeAll: true); } // Get the currently applied RICO settings (local, author, mod). RICOBuilding currentData = RICOUtils.CurrentRICOSetting(currentSelection); if (currentData != null) { // Convert the 'live' prefab (instance in PrefabCollection) and update household count and builidng level for all current instances. Loading.convertPrefabs.ConvertPrefab(currentData, PrefabCollection <BuildingInfo> .FindLoaded(currentBuildingData.prefab.name)); UpdateHouseholds(currentBuildingData.prefab.name, currentData.level); } else { Debugging.Message("no current RICO settings to apply to prefab " + currentBuildingData); } // Force an update of all panels with current values. SettingsPanel.Panel.UpdateSelectedBuilding(currentSelection); }
/// <summary> /// Called by the game when level loading is complete. /// </summary> /// <param name="mode">Loading mode (e.g. game, editor, scenario, etc.)</param> public override void OnLevelLoaded(LoadMode mode) { // Alert the user to any mod conflicts. ModUtils.NotifyConflict(); // Don't do anything further if mod hasn't activated (conflicting mod detected, or loading into editor instead of game). if (!isModEnabled) { return; } base.OnLevelLoaded(mode); // Don't do anything if in asset editor. if (mode == LoadMode.NewAsset || mode == LoadMode.LoadAsset) { return; } // Wait for loading to fully complete. while (!LoadingManager.instance.m_loadingComplete) { } // Check watchdog flag. if (!patchOperating) { // Patch wasn't operating; display warning notification and exit. HarmonyNotification notification = new HarmonyNotification(); notification.Create(); notification.Show(); return; } // Init Ploppable Tool panel. PloppableTool.Initialize(); // Add buttons to access building details from zoned building info panels. SettingsPanel.AddInfoPanelButtons(); // Deactivate the ploppable panel as it starts hidden. Don't need to deactivate the settings panel as it's not instantiated until first shown. PloppableTool.Instance.gameObject.SetActive(false); // Report any loading errors. Debugging.ReportErrors(); Debugging.Message("loading complete"); // Load settings file and check if we need to display update notification. settingsFile = Configuration <SettingsFile> .Load(); if (settingsFile.NotificationVersion != 2) { // No update notification "Don't show again" flag found; show the notification. UpdateNotification notification = new UpdateNotification(); notification.Create(); notification.Show(); } }
/// <summary> /// Returns the translation for the given key in the current language. /// </summary> /// <param name="key">Translation key to transate</param> /// <returns>Translation </returns> public string Translate(string key) { // Check that a valid current language is set. if (currentLanguage != null) { // Check that the current key is included in the translation. if (currentLanguage.translationDictionary.ContainsKey(key)) { // All good! Return translation. return(currentLanguage.translationDictionary[key]); } else { Debugging.Message("no translation for language " + currentLanguage.uniqueName + " found for key " + key); // Attempt fallack language; if even that fails, just return the key. return(FallbackLanguage().translationDictionary.ContainsKey(key) ? FallbackLanguage().translationDictionary[key] ?? key : key); } } else { Debugging.Message("no current language set when translating key " + key); } // If we've made it this far, something went wrong; just return the key. return(key); }
/// <summary> /// Postfix to attempt to catch issues where homecounts seem to be reset to zero for specific buildings. /// </summary> /// <param name="__instance">Original object instance reference</param> /// <param name="buildingID">Building instance ID</param> /// <param name="data">Building data</param> /// <param name="version">Version</param> private static void Postfix(PrivateBuildingAI __instance, ushort buildingID, ref Building data, uint version) { // Check to see if this is one of ours. if (__instance is GrowableResidentialAI) { // Check to see if no citizen units are set. if (data.m_citizenUnits == 0) { // Uh oh... Debugging.Message("no citizenUnits for building " + buildingID + " : " + data.Info.name + "; attempting reset"); // Backup resetOnLoad setting and force to true for reset attempt. bool oldReset = ModSettings.resetOnLoad; ModSettings.resetOnLoad = true; // Attempt reset for this building. Prefix(__instance, buildingID, ref data, version); // Restore original resetOnLoad seeting. ModSettings.resetOnLoad = oldReset; } else { Debugging.Message("building " + buildingID + " : " + data.Info.name + " passed CitizenUnits check"); } } }
/// <summary> /// Adds a Ploppable RICO button to a building info panel to directly access that building's RICO settings. /// The button will be added to the right of the panel with a small margin from the panel edge, at the relative Y position specified. /// </summary> /// <param name="infoPanel">Infopanel to apply the button to</param> /// <param name="relativeY">The relative Y position of the button within the panel</param> private static void AddInfoPanelButton(BuildingWorldInfoPanel infoPanel) { UIButton panelButton = infoPanel.component.AddUIComponent <UIButton>(); // Basic button setup. panelButton.size = new Vector2(34, 34); panelButton.normalBgSprite = "ToolbarIconGroup6Normal"; panelButton.normalFgSprite = "IconPolicyBigBusiness"; panelButton.focusedBgSprite = "ToolbarIconGroup6Focused"; panelButton.hoveredBgSprite = "ToolbarIconGroup6Hovered"; panelButton.pressedBgSprite = "ToolbarIconGroup6Pressed"; panelButton.disabledBgSprite = "ToolbarIconGroup6Disabled"; panelButton.name = "PloppableButton"; panelButton.tooltip = Translations.Translate("PRR_SET_RICO"); // Find ProblemsPanel relative position to position button. // We'll use 40f as a default relative Y in case something doesn't work. UIComponent problemsPanel; float relativeY = 40f; // Player info panels have wrappers, zoned ones don't. UIComponent wrapper = infoPanel.Find("Wrapper"); if (wrapper == null) { problemsPanel = infoPanel.Find("ProblemsPanel"); } else { problemsPanel = wrapper.Find("ProblemsPanel"); } try { // Position button vertically in the middle of the problems panel. If wrapper panel exists, we need to add its offset as well. relativeY = (wrapper == null ? 0 : wrapper.relativePosition.y) + problemsPanel.relativePosition.y + ((problemsPanel.height - 34) / 2); } catch { // Don't really care; just use default relative Y. Debugging.Message("couldn't find ProblemsPanel relative position"); } // Set position. panelButton.AlignTo(infoPanel.component, UIAlignAnchor.TopRight); panelButton.relativePosition += new Vector3(-5f, relativeY, 0f); // Event handler. panelButton.eventClick += (control, clickEvent) => { // Select current building in the building details panel and show. Open(InstanceManager.GetPrefabInfo(WorldInfoPanel.GetCurrentInstanceID()) as BuildingInfo); // Manually unfocus control, otherwise it can stay focused until next UI event (looks untidy). control.Unfocus(); }; }
/// <summary> /// Checks for known mod conflicts and function extenders. /// </summary> /// <returns>Whether or not Ploppable RICO should load</returns> internal static bool CheckMods() { // Check for conflicting mods. if (IsModEnabled(586012417ul)) { // Original Ploppable RICO mod detected. conflictingMod = true; Debugging.Message("Original Ploppable RICO detected - RICO Revisited exiting"); conflictMessage = Translations.Translate("PRR_CON_OPR") + " - " + Translations.Translate("PRR_CON_DWN") + "\r\n\r\n" + Translations.Translate("PRR_CON_ONE"); return(false); } else if (IsModInstalled("EnhancedBuildingCapacity")) { // Enhanced Building Capacity mod detected. conflictingMod = true; Debugging.Message("Enhanced Building Capacity mod detected - RICO Revisited exiting"); conflictMessage = Translations.Translate("PRR_CON_EBC") + " - " + Translations.Translate("PRR_CON_DWN") + "\r\n\r\n" + Translations.Translate("PRR_CON_ONE"); return(false); } else if (IsModInstalled("VanillaGarbageBinBlocker")) { // Garbage bin controller mod detected. conflictingMod = true; Debugging.Message("Garbage Bin Controller mod detected - RICO Revisited exiting"); conflictMessage = Translations.Translate("PRR_CON_GBC") + " - " + Translations.Translate("PRR_CON_DWN") + "\r\n\r\n" + Translations.Translate("PRR_CON_ONE"); return(false); } else if (IsModInstalled(1372431101ul)) { // Painter mod detected. conflictingMod = true; Debugging.Message("Painter detected - RICO Revisited exiting"); conflictMessage = Translations.Translate("PRR_CON_PTR") + " - " + Translations.Translate("PRR_CON_DWN") + "\r\n\r\n" + Translations.Translate("PRR_CON_PTR1"); return(false); } // No conflicts - now check for realistic population mods. realPopEnabled = (IsModInstalled("RealPopRevisited", true) || IsModInstalled("WG_BalancedPopMod", true)); // Check for Workshop RICO settings mod. if (IsModEnabled(629850626uL)) { Debugging.Message("found Workshop RICO settings mod"); Loading.mod1RicoDef = RICOReader.ParseRICODefinition("", Path.Combine(Util.SettingsModPath("629850626"), "WorkshopRICOSettings.xml"), false); } // Check for Ryuichi Kaminogi's "RICO Settings for Modern Japan CCP" Package modernJapanRICO = PackageManager.GetPackage("2035770233"); if (modernJapanRICO != null) { Debugging.Message("found RICO Settings for Modern Japan CCP"); Loading.mod2RicoDef = RICOReader.ParseRICODefinition("", Path.Combine(Path.GetDirectoryName(modernJapanRICO.packagePath), "PloppableRICODefinition.xml"), false); } return(true); }
/// <summary> /// Actions to update the UI on a language change go here. /// </summary> public void UpdateUILanguage() { Debugging.Message("setting language to " + (currentIndex < 0 ? "system" : languages.Values[currentIndex].uniqueName)); // UI update code goes here. // TOOO: Add dynamic UI update. // Update ploppable tool, if it's been created. PloppableTool.Instance?.SetText(); }
/// <summary> /// Updates household counts for all buildings in scene with the given prefab name. /// Can also remove all housholds, setting the total to zero. /// </summary> /// <param name="prefabName">Prefab name</param> /// <param name="removeAll">If true, all households will be removed (count set to 0)</param> private void UpdateHouseholds(string prefabName, int level = 0, bool removeAll = false) { int homeCount = 0; int visitCount = 0; int homeCountChanged = 0; // Get building manager instance. var instance = Singleton <BuildingManager> .instance; // Iterate through each building in the scene. for (ushort i = 0; i < instance.m_buildings.m_buffer.Length; i++) { // Check for matching name. if (instance.m_buildings.m_buffer[i].Info != null && instance.m_buildings.m_buffer[i].Info.name != null && instance.m_buildings.m_buffer[i].Info.name.Equals(prefabName)) { // Got a match! Check level if applicable. if (level > 0) { // m_level is one less than building.level. byte newLevel = (byte)(level - 1); if (instance.m_buildings.m_buffer[i].m_level != newLevel) { Debugging.Message("found building '" + prefabName + "' with level " + (instance.m_buildings.m_buffer[i].m_level + 1) + ", overriding to level " + level); instance.m_buildings.m_buffer[i].m_level = newLevel; } } // Update homecounts for any residential buildings. PrivateBuildingAI thisAI = instance.m_buildings.m_buffer[i].Info.GetAI() as ResidentialBuildingAI; if (thisAI != null) { // This is residential! If we're not removing all households, recalculate home and visit counts using AI method. if (!removeAll) { homeCount = thisAI.CalculateHomeCount((ItemClass.Level)instance.m_buildings.m_buffer[i].m_level, new Randomizer(i), instance.m_buildings.m_buffer[i].Width, instance.m_buildings.m_buffer[i].Length); visitCount = thisAI.CalculateVisitplaceCount((ItemClass.Level)instance.m_buildings.m_buffer[i].m_level, new Randomizer(i), instance.m_buildings.m_buffer[i].Width, instance.m_buildings.m_buffer[i].Length); } // Apply changes via direct call to EnsureCitizenUnits prefix patch from this mod and increment counter. RealisticCitizenUnits.EnsureCitizenUnits(ref thisAI, i, ref instance.m_buildings.m_buffer[i], homeCount, 0, visitCount, 0); homeCountChanged++; } // Clear any problems. instance.m_buildings.m_buffer[i].m_problems = 0; } } Debugging.Message("set household counts to " + homeCount + " for " + homeCountChanged + " '" + prefabName + "' buildings"); }
public static void UnpatchAll() { // Only unapply if patches appplied. if (_patched) { Debugging.Message("reverting Harmony patches"); // Unapply patches, but only with our HarmonyID. Harmony harmonyInstance = new Harmony(harmonyID); harmonyInstance.UnpatchAll(harmonyID); _patched = false; } }
/// <summary> /// Constructor. /// </summary> public ThumbnailGenerator() { if (ModSettings.debugLogging) { Debugging.Message("creating thumbnail generator"); } // Get local reference from parent. renderer = ThumbnailManager.Renderer; // Size and setting for thumbnail images: 109 x 100, doubled for anti-aliasing. renderer.Size = new Vector2(109, 100) * 2f; renderer.CameraRotation = 30f; }
/// <summary> /// Simple Prefix patch to catch Monuments panel setup exceptions. /// All we do is call (via reverse patch) the original method and painlessly catch any exceptions. /// </summary> /// <param name="__instance">Harmony original instance reference</param> /// <returns></returns> private static bool Prefix(UnlockingPanel __instance) { try { RefreshMonumentsPanelRev(__instance); } catch { Debugging.Message("caught monuments panel exception"); } // Don't call base method after this. return(false); }
/// <summary> /// Harmony transpiler removing two checks from BuildingInfo.InitializePrefab. /// </summary> /// <param name="instructions">CIL code to alter.</param> /// <returns></returns> private static IEnumerable <CodeInstruction> Transpiler(IEnumerable <CodeInstruction> instructions) { // The checks we're targeting for removal are fortunately clearly defined by their exception operands. // We're only going to remove the exception throw itself (including stack loading), and not the preceeding conditional check. // This minimises our footprint and reduces the chance of conflict with other transpilers, // and also means we don't have to worry about dealing with any branches to the start of the check. string[] targetOperands = { "Private building cannot have manual placement style", "Private building cannot include roads or other net types" }; var codes = new List <CodeInstruction>(instructions); // Deal with each of the operands consecutively and independently to avoid risk of error. foreach (string targetOperand in targetOperands) { // Stores the number of operands to cut. int cutCount = 0; // Iterate through each opcode in the CIL, looking for an ldarg.0 immediately followed by an ldstr. for (int i = 0; i < codes.Count; i++) { if ((codes[i].opcode == OpCodes.Ldarg_0) && (codes[i + 1].opcode == OpCodes.Ldstr)) { // Found a matching combo - now check the ldstr operand aginst our target. if (codes[i + 1].operand.Equals(targetOperand)) { // Operand match - now count forward from this operand until we encounter an exception throw. while (codes[i + cutCount].opcode != OpCodes.Throw) { cutCount++; } Debugging.Message("InitPrefab transpiler removing CIL (offset " + cutCount + ") from " + i + " (" + codes[i].opcode + " to " + codes[i + cutCount].opcode + ") - " + targetOperand); // Remove the CIL from the ldarg.0 to the throw (inclusive). // +1 to avoid fencepost error (need to include original instruction as well). codes.RemoveRange(i, cutCount + 1); // We're done with this one - no point in continuing the loop. break; } } } } return(codes.AsEnumerable()); }
/// <summary> /// Regenerates all thumbnails. /// Useful for e.g. regenerating thumbnails. /// </summary> internal void RegenerateThumbnails() { // Only do this if the ploppable tool has been created. if (Instance != null) { Debugging.Message("regenerating all thumbnails"); // Step through each loaded and active RICO prefab. foreach (BuildingData buildingData in Loading.xmlManager.prefabHash.Values) { // Cancel its atlas. buildingData.thumbnailAtlas = null; } } }
/// <summary> /// Fills the Ploppable Tool panel with building buttons. /// </summary> private void PopulateButtons() { Debugging.Message("populating building buttons"); // Step through each loaded and active RICO prefab. foreach (var buildingData in Loading.xmlManager.prefabHash.Values) { if (buildingData != null) { // Get the prefab. var prefab = PrefabCollection <BuildingInfo> .FindLoaded(buildingData.name); // Local settings first. if (buildingData.hasLocal) { // Only if enabled. if (buildingData.local.ricoEnabled) { // Add button to panel and remove any existing UI button (in other base game ploppable panels). AddBuildingButton(buildingData, buildingData.local.uiCategory); RemoveUIButton(prefab); continue; } } // Author settings second. else if (buildingData.hasAuthor) { // Only if enabled. if (buildingData.author.ricoEnabled) { // Add button to panel and remove any existing UI button (in other base game ploppable panels). AddBuildingButton(buildingData, buildingData.author.uiCategory); RemoveUIButton(prefab); continue; } } // Mod settings third. Don't have to worry about enablement here. else if (buildingData.hasMod) { AddBuildingButton(buildingData, buildingData.mod.uiCategory); RemoveUIButton(prefab); } } } // Set active tab as default. TabClicked(BuildingPanels[0], TabSprites[0]); }
/// <summary> /// Save settings to XML file. /// </summary> internal static void SaveSettings() { try { // Pretty straightforward. Serialisation is within GBRSettingsFile class. using (StreamWriter writer = new StreamWriter(SettingsFileName)) { XmlSerializer xmlSerializer = new XmlSerializer(typeof(XMLSettingsFile)); xmlSerializer.Serialize(writer, new XMLSettingsFile()); } } catch (Exception e) { Debugging.Message("exception saving XML settings file"); Debugging.LogException(e); } }
/// <summary> /// Returns the translation for the given key in the current language. /// </summary> /// <param name="key">Translation key to transate</param> /// <returns>Translation </returns> public string Translate(string key) { Language currentLanguage; // Check to see if we're using system settings. if (currentIndex < 0) { // Using system settings - initialise system language if we haven't already. if (systemLanguage == null) { SetSystemLanguage(); } currentLanguage = systemLanguage; } else { currentLanguage = languages.Values[currentIndex]; } // Check that a valid current language is set. if (currentLanguage != null) { // Check that the current key is included in the translation. if (currentLanguage.translationDictionary.ContainsKey(key)) { // All good! Return translation. return(currentLanguage.translationDictionary[key]); } else { Debugging.Message("no translation for language " + currentLanguage.uniqueName + " found for key " + key); // Attempt fallack translation. return(FallbackTranslation(currentLanguage.uniqueName, key)); } } else { Debugging.Message("no current language when translating key " + key); } // If we've made it this far, something went wrong; just return the key. return(key); }
/// <summary> /// Creates the panel object in-game and displays it. /// </summary> internal static void Open(BuildingInfo selected = null) { try { // If no instance already set, create one. if (uiGameObject == null) { // Give it a unique name for easy finding with ModTools. uiGameObject = new GameObject("RICOSettingsPanel"); uiGameObject.transform.parent = UIView.GetAView().transform; _panel = uiGameObject.AddComponent <RICOSettingsPanel>(); // Set up panel. Panel.Setup(); } // Select appropriate building if there's a preselection. if (selected != null) { Debugging.Message("selecting preselected building " + selected.name); Panel.SelectBuilding(selected); } else if (lastSelection != null) { Panel.SelectBuilding(lastSelection); // Restore previous filter state. if (lastFilter != null) { Panel.SetFilter(lastFilter); } // Restore previous building selection list postion and selected item. Panel.SetListPosition(lastIndex, lastPostion); } Panel.Show(); } catch (Exception e) { Debugging.LogException(e); return; } }
/// <summary> /// Loads languages from XML files. /// </summary> private void LoadLanguages() { // Clear existing dictionary. languages.Clear(); // Get the current assembly path and append our locale directory name. string assemblyPath = GetAssemblyPath(); if (!assemblyPath.IsNullOrWhiteSpace()) { string localePath = Path.Combine(assemblyPath, "Translations"); // Ensure that the directory exists before proceeding. if (Directory.Exists(localePath)) { // Load each file in directory and attempt to deserialise as a translation file. string[] translationFiles = Directory.GetFiles(localePath); foreach (string translationFile in translationFiles) { using (StreamReader reader = new StreamReader(translationFile)) { XmlSerializer xmlSerializer = new XmlSerializer(typeof(Language)); if (xmlSerializer.Deserialize(reader) is Language translation) { // Got one! add it to the list. languages.Add(translation.uniqueName, translation); } else { Debugging.Message("couldn't deserialize translation file '" + translationFile); } } } } else { Debugging.Message("translations directory not found"); } } else { Debugging.Message("assembly path was empty"); } }
/// <summary> /// Removes a given prefab's existing UI button, if any (e.g. from the game's Park menu if the prefab was originally a park, etc.) /// </summary> /// <param name="prefab">Building prefab</param> private void RemoveUIButton(BuildingInfo prefab) { UIButton refButton = new UIButton(); if (prefab?.name != null) { Debugging.Message("attempting to find UI button for " + prefab.name); // Find any existing UI component linked to this prefab. refButton = UIView.GetAView()?.FindUIComponent <UIButton>(prefab.name); } if (refButton != null) { // We found one - destry it. Debugging.Message("destroying UI button for " + prefab.name); refButton.isVisible = false; GameObject.Destroy(refButton.gameObject); } }
/// <summary> /// Destroys all building buttons and generates a new set. /// Useful for e.g. regenerating thumbnails. /// </summary> internal void RebuildButtons() { // Only do this if the ploppable tool has been created. if (Instance != null) { Debugging.Message("destroying all building buttons"); // Step through each loaded and active RICO prefab. foreach (var buildingData in Loading.xmlManager.prefabHash.Values) { // Destroy all existing building buttons. if (buildingData.buildingButton != null) { GameObject.Destroy(buildingData.buildingButton); } } // Repopulate building buttons. Instance.PopulateButtons(); } }
/// <summary> /// Apply all Harmony patches. /// </summary> public static void PatchAll() { // Don't do anything if already patched. if (!_patched) { // Ensure Harmony is ready before patching. if (HarmonyHelper.IsHarmonyInstalled) { Debugging.Message("deploying Harmony patches"); // Apply all annotated patches and update flag. Harmony harmonyInstance = new Harmony(harmonyID); harmonyInstance.PatchAll(); _patched = true; } else { Debugging.Message("Harmony not ready"); } } }
/// <summary> /// Called by the game when the mod is initialised at the start of the loading process. /// </summary> /// <param name="loading">Loading mode (e.g. game, editor, scenario, etc.)</param> public override void OnCreated(ILoading loading) { // Don't do anything if not in game (e.g. if we're going into an editor). if (loading.currentMode != AppMode.Game) { isModEnabled = false; Debugging.Message("not loading into game, skipping activation"); } else { // Check for conflicting (and other) mods. isModEnabled = ModUtils.CheckMods(); } // If we're not enabling the mod due to one of the above checks failing, unapply Harmony patches before returning without doing anything. if (!isModEnabled) { Patcher.UnpatchAll(); return; } // Make sure patches have been applied before proceeding. if (!Patcher.Patched) { Debugging.Message("Harmony patches not applied, exiting"); isModEnabled = false; return; } // Otherwise, game on! Debugging.Message("v" + PloppableRICOMod.Version + " loading"); // Ensure patch watchdog flag is properly initialised. patchOperating = false; // Create instances if they don't already exist. if (convertPrefabs == null) { convertPrefabs = new ConvertPrefabs(); } if (xmlManager == null) { xmlManager = new RICOPrefabManager { prefabHash = new Dictionary <BuildingInfo, BuildingData>(), prefabList = new List <BuildingData>() }; } // Read mod settings. SettingsFile settingsFile = Configuration <SettingsFile> .Load(); Settings.plainThumbs = settingsFile.PlainThumbs; Settings.debugLogging = settingsFile.DebugLogging; Settings.resetOnLoad = settingsFile.ResetOnLoad; // Read any local RICO settings. string ricoDefPath = "LocalRICOSettings.xml"; localRicoDef = null; if (!File.Exists(ricoDefPath)) { Debugging.Message("no " + ricoDefPath + " file found"); } else { localRicoDef = RICOReader.ParseRICODefinition("", ricoDefPath, insanityOK: true); if (localRicoDef == null) { Debugging.Message("no valid definitions in " + ricoDefPath); } } base.OnCreated(loading); }
/// <summary> /// Harmony Transpiler to add checks to see if extractor buildings should be demolished if they're outside of a district with relevant specialization settings. /// </summary> /// <param name="instructions">Original ILCode</param> /// <returns>Replacement (patched) ILCode</returns> private static IEnumerable <CodeInstruction> Transpiler(IEnumerable <CodeInstruction> instructions) { Debugging.Message("transpiler patching specialized building checks in IndustrialExtractorAI.SimulationStep"); return(CheckSpecTranspiler.Transpiler(instructions)); }
/// <summary> /// Interpret and apply RICO settings to a building prefab. /// </summary> /// <param name="buildingData">RICO building data to apply</param> /// <param name="prefab">The building prefab to be changed</param> internal void ConvertPrefab(RICOBuilding buildingData, BuildingInfo prefab) { // AI class for prefab init. string aiClass; if (prefab != null) { // Check eligibility for any growable assets. if (buildingData.growable) { // Growables can't have any dimension greater than 4. if (prefab.GetWidth() > 4 || prefab.GetLength() > 4) { buildingData.growable = false; Debugging.Message("building '" + prefab.name + "' can't be growable because it is too big"); } // Growables can't have net structures. if (prefab.m_paths != null && prefab.m_paths.Length != 0) { buildingData.growable = false; Debugging.Message("building '" + prefab.name + "' can't be growable because it contains network assets"); } } // Apply AI based on service. switch (buildingData.service) { // Dummy AI. case "dummy": // Get AI. DummyBuildingAI dummyAI = prefab.gameObject.AddComponent <DummyBuildingAI>(); // Use beautification ItemClass to avoid issues, and never make growable. InitializePrefab(prefab, dummyAI, "Beautification Item", false); // Final circular reference. prefab.m_buildingAI.m_info = prefab; // Dummy is a special case, and we're done here. return; // Residential AI. case "residential": // Get AI. GrowableResidentialAI residentialAI = buildingData.growable ? prefab.gameObject.AddComponent <GrowableResidentialAI>() : prefab.gameObject.AddComponent <PloppableResidentialAI>(); if (residentialAI == null) { throw new Exception("Ploppable RICO residential AI not found."); } // Assign basic parameters. residentialAI.m_ricoData = buildingData; residentialAI.m_constructionCost = buildingData.constructionCost; residentialAI.m_homeCount = buildingData.homeCount; // Determine AI class string according to subservice. switch (buildingData.subService) { case "low eco": // Apply eco service if GC installed, otherwise use normal low residential. if (Util.isGCinstalled()) { aiClass = "Low Residential Eco - Level"; } else { aiClass = "Low Residential - Level"; } break; case "high eco": // Apply eco service if GC installed, otherwise use normal high residential. if (Util.isGCinstalled()) { aiClass = "High Residential Eco - Level"; } else { aiClass = "High Residential - Level"; } break; case "high": // Stock standard high commercial. aiClass = "High Residential - Level"; break; default: // Fall back to low residential as default. aiClass = "Low Residential - Level"; // If invalid subservice, report. if (buildingData.subService != "low") { Debugging.ErrorBuffer.AppendLine("Residential building " + buildingData.name + " has invalid subservice " + buildingData.subService + "; reverting to low residential."); } break; } // Initialize the prefab. InitializePrefab(prefab, residentialAI, aiClass + buildingData.level, buildingData.growable); break; // Office AI. case "office": // Get AI. GrowableOfficeAI officeAI = buildingData.growable ? prefab.gameObject.AddComponent <GrowableOfficeAI>() : prefab.gameObject.AddComponent <PloppableOfficeAI>(); if (officeAI == null) { throw new Exception("Ploppable RICO Office AI not found."); } // Assign basic parameters. officeAI.m_ricoData = buildingData; officeAI.m_workplaceCount = buildingData.workplaceCount; officeAI.m_constructionCost = buildingData.constructionCost; // Check if this is an IT Cluster specialisation. // Determine AI class string according to subservice. if (buildingData.subService == "high tech") { // Apply IT cluster if GC installed, otherwise use Level 3 office. if (Util.isGCinstalled()) { aiClass = "Office - Hightech"; } else { aiClass = "Office - Level3"; } } else { // Not IT cluster - boring old ordinary office. aiClass = "Office - Level" + buildingData.level; } // Initialize the prefab. InitializePrefab(prefab, officeAI, aiClass, buildingData.growable); break; // Industrial AI. case "industrial": // Get AI. GrowableIndustrialAI industrialAI = buildingData.growable ? prefab.gameObject.AddComponent <GrowableIndustrialAI>() : prefab.gameObject.AddComponent <PloppableIndustrialAI>(); if (industrialAI == null) { throw new Exception("Ploppable RICO Industrial AI not found."); } // Assign basic parameters. industrialAI.m_ricoData = buildingData; industrialAI.m_workplaceCount = buildingData.workplaceCount; industrialAI.m_constructionCost = buildingData.constructionCost; industrialAI.m_pollutionEnabled = buildingData.pollutionEnabled; // Determine AI class string according to subservice. // Check for valid subservice. if (IsValidIndSubServ(buildingData.subService)) { // Specialised industry. aiClass = ServiceName(buildingData.subService) + " - Processing"; } else { // Generic industry. aiClass = "Industrial - Level" + buildingData.level; } // Initialize the prefab. InitializePrefab(prefab, industrialAI, aiClass, buildingData.growable); break; // Extractor AI. case "extractor": // Get AI. GrowableExtractorAI extractorAI = buildingData.growable ? prefab.gameObject.AddComponent <GrowableExtractorAI>() : prefab.gameObject.AddComponent <PloppableExtractorAI>(); if (extractorAI == null) { throw new Exception("Ploppable RICO Extractor AI not found."); } // Assign basic parameters. extractorAI.m_ricoData = buildingData; extractorAI.m_workplaceCount = buildingData.workplaceCount; extractorAI.m_constructionCost = buildingData.constructionCost; extractorAI.m_pollutionEnabled = buildingData.pollutionEnabled; // Check that we have a valid industry subservice. if (IsValidIndSubServ(buildingData.subService)) { // Initialise the prefab. InitializePrefab(prefab, extractorAI, ServiceName(buildingData.subService) + " - Extractor", buildingData.growable); } else { Debugging.Message("invalid industry subservice " + buildingData.subService + " for extractor " + buildingData.name); } break; // Commercial AI. case "commercial": // Get AI. GrowableCommercialAI commercialAI = buildingData.growable ? prefab.gameObject.AddComponent <GrowableCommercialAI>() : prefab.gameObject.AddComponent <PloppableCommercialAI>(); if (commercialAI == null) { throw new Exception("Ploppable RICO Commercial AI not found."); } // Assign basic parameters. commercialAI.m_ricoData = buildingData; commercialAI.m_workplaceCount = buildingData.workplaceCount; commercialAI.m_constructionCost = buildingData.constructionCost; // Determine AI class string according to subservice. switch (buildingData.subService) { // Organic and Local Produce. case "eco": // Apply eco specialisation if GC installed, otherwise use Level 1 low commercial. if (Util.isGCinstalled()) { // Eco commercial buildings only import food goods. commercialAI.m_incomingResource = TransferManager.TransferReason.Food; aiClass = "Eco Commercial"; } else { aiClass = "Low Commercial - Level1"; } break; // Tourism. case "tourist": // Apply tourist specialisation if AD installed, otherwise use Level 1 low commercial. if (Util.isADinstalled()) { aiClass = "Tourist Commercial - Land"; } else { aiClass = "Low Commercial - Level1"; } break; // Leisure. case "leisure": // Apply leisure specialisation if AD installed, otherwise use Level 1 low commercial. if (Util.isADinstalled()) { aiClass = "Leisure Commercial"; } else { aiClass = "Low Commercial - Level1"; } break; // Bog standard high commercial. case "high": aiClass = "High Commercial - Level" + buildingData.level; break; // Fall back to low commercial as default. default: aiClass = "Low Commercial - Level" + buildingData.level; // If invalid subservice, report. if (buildingData.subService != "low") { Debugging.ErrorBuffer.AppendLine("Commercial building " + buildingData.name + " has invalid subService " + buildingData.subService + "; reverting to low commercial."); } break; } // Initialize the prefab. InitializePrefab(prefab, commercialAI, aiClass, buildingData.growable); break; } } }
/// <summary> /// Interpret and apply RICO settings to a building prefab. /// </summary> /// <param name="buildingData">RICO building data to apply</param> /// <param name="prefab">The building prefab to be changed</param> internal void ConvertPrefab(RICOBuilding buildingData, BuildingInfo prefab) { if (prefab != null) { // Check eligibility for any growable assets. if (buildingData.growable) { // Growables can't have any dimension greater than 4. if (prefab.GetWidth() > 4 || prefab.GetLength() > 4) { buildingData.growable = false; Debugging.Message("building '" + prefab.name + "' can't be growable because it is too big"); } // Growables can't have net structures. if (prefab.m_paths != null && prefab.m_paths.Length != 0) { buildingData.growable = false; Debugging.Message("building '" + prefab.name + "' can't be growable because it contains network assets"); } } if (buildingData.service == "dummy") { var ai = prefab.gameObject.AddComponent <DummyBuildingAI>(); // Use beautification ItemClass to avoid issues, and never make growable. InitializePrefab(prefab, ai, "Beautification Item", false); // Final circular reference. prefab.m_buildingAI.m_info = prefab; } else if (buildingData.service == "residential") { var ai = buildingData.growable ? prefab.gameObject.AddComponent <GrowableResidentialAI>() : prefab.gameObject.AddComponent <PloppableResidentialAI>(); if (ai == null) { throw (new Exception("Residential-AI not found.")); } ai.m_ricoData = buildingData; ai.m_constructionCost = buildingData.constructionCost; ai.m_homeCount = buildingData.homeCount; if (buildingData.subService == "low eco") { // Apply eco service if GC installed, otherwise use normal low residential. if (Util.isGCinstalled()) { InitializePrefab(prefab, ai, "Low Residential Eco - Level" + buildingData.level, buildingData.growable); } else { InitializePrefab(prefab, ai, "Low Residential - Level" + buildingData.level, buildingData.growable); } } else if (buildingData.subService == "high eco") { // Apply eco service if GC installed, otherwise use normal high residential. if (Util.isGCinstalled()) { InitializePrefab(prefab, ai, "High Residential Eco - Level" + buildingData.level, buildingData.growable); } else { InitializePrefab(prefab, ai, "High Residential - Level" + buildingData.level, buildingData.growable); } } else if (buildingData.subService == "high") { // Stock standard high commercial. InitializePrefab(prefab, ai, "High Residential - Level" + buildingData.level, buildingData.growable); } else { // Fall back to low residential as default. InitializePrefab(prefab, ai, "Low Residential - Level" + buildingData.level, buildingData.growable); // If invalid subservice, report. if (buildingData.subService != "low") { Debugging.ErrorBuffer.AppendLine("Residential building " + buildingData.name + " has invalid subservice " + buildingData.subService + "; reverting to low residential."); } } } else if (buildingData.service == "office") { var ai = buildingData.growable ? prefab.gameObject.AddComponent <GrowableOfficeAI>() : prefab.gameObject.AddComponent <PloppableOfficeAI>(); if (ai == null) { throw (new Exception("Office-AI not found.")); } ai.m_ricoData = buildingData; ai.m_workplaceCount = buildingData.workplaceCount; ai.m_constructionCost = buildingData.constructionCost; if (buildingData.subService == "high tech") { // Apply IT cluster if GC installed, otherwise use Level 3 office. if (Util.isGCinstalled()) { InitializePrefab(prefab, ai, "Office - Hightech", buildingData.growable); } else { InitializePrefab(prefab, ai, "Office - Level3", buildingData.growable); } } else { // Not IT cluster - boring old ordinary office. InitializePrefab(prefab, ai, "Office - Level" + buildingData.level, buildingData.growable); } } else if (buildingData.service == "industrial") { var ai = buildingData.growable ? prefab.gameObject.AddComponent <GrowableIndustrialAI>() : prefab.gameObject.AddComponent <PloppableIndustrialAI>(); if (ai == null) { throw (new Exception("Industrial-AI not found.")); } ai.m_ricoData = buildingData; ai.m_workplaceCount = buildingData.workplaceCount; ai.m_constructionCost = buildingData.constructionCost; ai.m_pollutionEnabled = buildingData.pollutionEnabled; if (Util.industryServices.Contains(buildingData.subService)) { InitializePrefab(prefab, ai, Util.ucFirst(buildingData.subService) + " - Processing", buildingData.growable); } else { InitializePrefab(prefab, ai, "Industrial - Level" + buildingData.level, buildingData.growable); } } else if (buildingData.service == "extractor") { var ai = buildingData.growable ? prefab.gameObject.AddComponent <GrowableExtractorAI>() : prefab.gameObject.AddComponent <PloppableExtractorAI>(); if (ai == null) { throw (new Exception("Extractor-AI not found.")); } ai.m_ricoData = buildingData; ai.m_workplaceCount = buildingData.workplaceCount; ai.m_constructionCost = buildingData.constructionCost; ai.m_pollutionEnabled = buildingData.pollutionEnabled; if (Util.industryServices.Contains(buildingData.subService)) { InitializePrefab(prefab, ai, Util.ucFirst(buildingData.subService) + " - Extractor", buildingData.growable); } } else if (buildingData.service == "commercial") { var ai = buildingData.growable ? prefab.gameObject.AddComponent <GrowableCommercialAI>() : prefab.gameObject.AddComponent <PloppableCommercialAI>(); if (ai == null) { throw (new Exception("Commercial-AI not found.")); } ai.m_ricoData = buildingData; ai.m_workplaceCount = buildingData.workplaceCount; ai.m_constructionCost = buildingData.constructionCost; if (buildingData.subService == "eco") { // Apply eco specialisation if GC installed, otherwise use Level 1 low commercial. if (Util.isGCinstalled()) { // Eco commercial buildings only import food goods. ai.m_incomingResource = TransferManager.TransferReason.Food; InitializePrefab(prefab, ai, "Eco Commercial", buildingData.growable); } else { InitializePrefab(prefab, ai, "Low Commercial - Level1", buildingData.growable); } } else if (buildingData.subService == "tourist") { // Apply tourist specialisation if AD installed, otherwise use Level 1 low commercial. if (Util.isADinstalled()) { InitializePrefab(prefab, ai, "Tourist Commercial - Land", buildingData.growable); } else { InitializePrefab(prefab, ai, "Low Commercial - Level1", buildingData.growable); } } else if (buildingData.subService == "leisure") { // Apply leisure specialisation if AD installed, otherwise use Level 1 low commercial. if (Util.isADinstalled()) { InitializePrefab(prefab, ai, "Leisure Commercial", buildingData.growable); } else { InitializePrefab(prefab, ai, "Low Commercial - Level1", buildingData.growable); } } else if (buildingData.subService == "high") { // Bog standard high commercial. InitializePrefab(prefab, ai, "High Commercial - Level" + buildingData.level, buildingData.growable); } else { // Fall back to low commercial as default. InitializePrefab(prefab, ai, "Low Commercial - Level" + buildingData.level, buildingData.growable); // If invalid subservice, report. if (buildingData.subService != "low") { Debugging.ErrorBuffer.AppendLine("Commercial building " + buildingData.name + " has invalid subService " + buildingData.subService + "; reverting to low commercial."); } } } } }