Beispiel #1
0
        /// <summary>
        /// Calculates the construction cost of a workplace, depending on current settings (overrides or default).
        /// </summary>
        /// <param name="thisAI">AI reference to calculate for</param>
        /// <returns>Final construction cost</returns>
        internal static int WorkplaceConstructionCost(PrivateBuildingAI thisAI, int fixedCost)
        {
            int baseCost;

            // Local references.
            BuildingInfo thisInfo = thisAI.m_info;

            ItemClass.Level thisLevel = thisInfo.GetClassLevel();

            // Are we overriding cost?
            if (ModSettings.overrideCost)
            {
                // Yes - calculate based on workplaces by level multiplied by appropriate cost-per-job setting.
                thisAI.CalculateWorkplaceCount(thisLevel, new Randomizer(), thisInfo.GetWidth(), thisInfo.GetLength(), out int jobs0, out int jobs1, out int jobs2, out int jobs3);
                baseCost = (ModSettings.costPerJob0 * jobs0) + (ModSettings.costPerJob1 * jobs1) + (ModSettings.costPerJob2 * jobs2) + (ModSettings.costPerJob3 * jobs3);
            }
            else
            {
                // No - just use the base cost provided.
                baseCost = fixedCost;
            }

            // Multiply base cost by 100 before feeding to EconomyManager for nomalization to game conditions prior to return.
            baseCost *= 100;
            Singleton <EconomyManager> .instance.m_EconomyWrapper.OnGetConstructionCost(ref baseCost, thisInfo.GetService(), thisInfo.GetSubService(), thisLevel);

            return(baseCost);
        }
        /// <summary>
        /// Harmony Prefix patch to IndustrialBuildingAI.CalculateProductionCapacity to implement mod production calculations.
        /// </summary>
        /// <param name="__result">Original method result</param>
        /// <param name="__instance">Original AI instance reference</param>
        /// <param name="level">Building level</param>
        /// <returns>False (don't execute base game method after this)</returns>
        public static bool Prefix(ref int __result, IndustrialBuildingAI __instance, ItemClass.Level level, int width, int length)
        {
            // Get builidng info.
            BuildingInfo info = __instance.m_info;

            ItemClass.SubService subService = info.GetSubService();

            // Array index.
            int arrayIndex = GetIndex(subService);

            // New or old method?
            if (prodModes[arrayIndex] == (int)ProdModes.popCalcs)
            {
                // New settings, based on population.
                float multiplier;
                switch (info.GetClassLevel())
                {
                case ItemClass.Level.Level1:
                    multiplier = 1f;
                    break;

                case ItemClass.Level.Level2:
                    multiplier = 0.933333f;
                    break;

                default:
                    multiplier = 0.8f;
                    break;
                }

                // Get cached workplace count and calculate total workplaces.
                int[] workplaces = PopData.instance.WorkplaceCache(info, (int)level);

                float totalWorkers = workplaces[0] + workplaces[1] + workplaces[2] + workplaces[3];
                // Multiply total workers by multipler and overall multiplier (from settings) to get result.
                __result = (int)((totalWorkers * multiplier * prodMults[arrayIndex]) / 100f);
            }
            else
            {
                // Legacy calcs.
                int[] array = LegacyAIUtils.GetIndustryArray(__instance.m_info, (int)level);

                // Original method return value.
                __result = Mathf.Max(100, width * length * array[DataStore.PRODUCTION]) / 100;
            }


            // Always set at least one.
            if (__result < 1)
            {
                Logging.Error("invalid production result ", __result.ToString(), " for ", __instance.m_info.name, "; setting to 1");
                __result = 1;
            }

            // Don't execute base method after this.
            return(false);
        }
Beispiel #3
0
        /// <summary>
        /// Returns the calculated visitplace count according to current settings for the given prefab and workforce total (e.g. for previewing effects of changes to workforces).
        /// </summary>
        /// <param name="prefab">Prefab to check</param>
        /// <param name="workplaces">Number of workplaces to apply</param>
        /// <returns>Calculated visitplaces</returns>
        internal static int PreviewVisitCount(BuildingInfo prefab, int workplaces)
        {
            // Get builidng info.
            ItemClass.SubService subService = prefab.GetSubService();

            // Array index.
            int arrayIndex = GetIndex(subService);

            // New or old calculations?
            if (comVisitModes[arrayIndex] == (int)ComVisitModes.popCalcs)
            {
                // New calcs.
                return(NewVisitCount(subService, prefab.GetClassLevel(), workplaces));
            }
            else
            {
                // Old calcs.
                return(LegacyVisitCount(prefab, prefab.GetClassLevel()));
            }
        }
Beispiel #4
0
        /// <summary>
        /// Returns a list of calculation packs available for the given prefab.
        /// </summary>
        /// <param name="prefab">BuildingInfo prefab</param>
        /// <returns>Array of available calculation packs</returns>
        internal SchoolDataPack[] GetPacks(BuildingInfo prefab)
        {
            // Return list.
            List <SchoolDataPack> list = new List <SchoolDataPack>();

            ItemClass.Level level = prefab.GetClassLevel();

            // Iterate through each floor pack and see if it applies.
            foreach (SchoolDataPack pack in calcPacks)
            {
                // Check for matching service.
                if (pack.level == level)
                {
                    // Service matches; add pack.
                    list.Add(pack);
                }
            }

            return(list.ToArray());
        }
Beispiel #5
0
        /// <summary>
        /// Sets the customised number of workers for a given prefab.
        /// If a record doesn't already exist, a new one will be created.
        /// </summary>
        /// <param name="prefab">The prefab (BuildingInfo) to set</param>
        /// <param name="workers">The updated worker count</param>
        public static void SetWorker(BuildingInfo prefab, int workers)
        {
            // Update or add entry to configuration file cache.
            if (DataStore.workerCache.ContainsKey(prefab.name))
            {
                // Prefab already has a record; update.
                DataStore.workerCache[prefab.name] = workers;
            }
            else
            {
                // Prefab doesn't already have a record; create.
                DataStore.workerCache.Add(prefab.name, workers);
            }

            // Save the updated configuration file.
            XMLUtilsWG.WriteToXML();

            // Get current building hash (for updating prefab dictionary).
            var prefabHash = prefab.gameObject.GetHashCode();

            // Calculate employment breakdown.
            int[] array = CommercialBuildingAIMod.GetArray(prefab, (int)prefab.GetClassLevel());
            PrefabEmployStruct output = new PrefabEmployStruct();

            AI_Utils.CalculateprefabWorkerVisit(prefab.GetWidth(), prefab.GetLength(), ref prefab, 4, ref array, out output);

            // Update entry in 'live' settings.
            if (DataStore.prefabWorkerVisit.ContainsKey(prefabHash))
            {
                // Prefab already has a record; update.
                DataStore.prefabWorkerVisit[prefabHash] = output;
            }
            else
            {
                // Prefab doesn't already have a record; create.
                DataStore.prefabWorkerVisit.Add(prefabHash, output);
            }
        }
        /// <summary>
        /// Updates the state of the service panel Realistic Population button - should only be visible and enabled when looking at elementary and high schools.
        /// </summary>
        internal static void UpdateServicePanelButton()
        {
            // Get current building instance.
            ushort building = WorldInfoPanel.GetCurrentInstanceID().Building;

            // Ensure valid building before proceeding.
            if (building > 0)
            {
                // Check for eduction service and level 1 or 2.
                BuildingInfo info = Singleton <BuildingManager> .instance.m_buildings.m_buffer[building].Info;
                if (info.GetService() == ItemClass.Service.Education && info.GetClassLevel() < ItemClass.Level.Level3)
                {
                    // It's a school!  Enable and show the button, and return.
                    serviceButton.Enable();
                    serviceButton.Show();
                    return;
                }
            }

            // If we got here, it's not a valid school building; disable and hide the button.
            serviceButton.Disable();
            serviceButton.Hide();
        }
Beispiel #7
0
        /// <summary>
        /// Updates all school prefabs (e.g. when the global multiplier has changed).
        /// </summary>
        internal void UpdateSchools()
        {
            // Iterate through all loaded building prefabs.
            for (uint i = 0; i < PrefabCollection <BuildingInfo> .LoadedCount(); ++i)
            {
                BuildingInfo building = PrefabCollection <BuildingInfo> .GetLoaded(i);

                // Check for schools.
                if (building?.name != null && building.GetAI() is SchoolAI schoolAI && (building.GetClassLevel() == ItemClass.Level.Level1 || building.GetClassLevel() == ItemClass.Level.Level2))
                {
                    // Found a school; update school record and tooltip.
                    UpdateSchoolPrefab(building, schoolAI);
                }
            }
        }
Beispiel #8
0
        /// <summary>
        /// Performs task on completion of level loading - recording of school default properties and application of our settings.
        /// Should be called OnLevelLoaded, after prefabs have been loaded but before gameplay commences.
        /// </summary>
        internal void OnLoad()
        {
            // Initialise original properties dictionary.
            originalStats = new Dictionary <string, OriginalSchoolStats>();

            // Iterate through all loaded building prefabs.
            for (uint i = 0; i < PrefabCollection <BuildingInfo> .LoadedCount(); ++i)
            {
                BuildingInfo building = PrefabCollection <BuildingInfo> .GetLoaded(i);

                // Check for schools.
                if (building?.name != null && building.GetAI() is SchoolAI schoolAI && (building.GetClassLevel() == ItemClass.Level.Level1 || building.GetClassLevel() == ItemClass.Level.Level2))
                {
                    // Found a school; add it to our dictionary.
                    originalStats.Add(building.name, new OriginalSchoolStats
                    {
                        jobs0       = schoolAI.m_workPlaceCount0,
                        jobs1       = schoolAI.m_workPlaceCount1,
                        jobs2       = schoolAI.m_workPlaceCount2,
                        jobs3       = schoolAI.m_workPlaceCount3,
                        cost        = schoolAI.m_constructionCost,
                        maintenance = schoolAI.m_maintenanceCost
                    });

                    // If setting is set, get currently active pack and apply it.
                    if (ModSettings.enableSchoolProperties)
                    {
                        ApplyPack(building, ActivePack(building) as SchoolDataPack);

                        // ApplyPack includes a call to UpdateSchoolPrefab, so no need to do it again here.
                        continue;
                    }

                    // Update school record and tooltip.
                    UpdateSchoolPrefab(building, schoolAI);
                }
            }
        }
Beispiel #9
0
 /// <summary>
 /// Returns the currently set default calculation pack for the given prefab.
 /// </summary>
 /// <param name="building">Building prefab</param>
 /// <returns>Default calculation data pack</returns>
 internal override DataPack CurrentDefaultPack(BuildingInfo building) => BaseDefaultPack(building.GetClassLevel(), building);
        /// <summary>
        /// Upgrades/downgrades the selected building to the given level, if possible.
        /// </summary>
        /// <param name="buildingID">Building instance ID</param>
        /// <param name="targetLevel">Level to upgrade/downgrade to</param>
        internal static void ForceLevel(ushort buildingID, byte targetLevel)
        {
            // BuildingInfo to change to, if this building isn't historical.
            BuildingInfo targetInfo = null;

            // References.
            BuildingManager buildingManager = Singleton <BuildingManager> .instance;

            Building[]        buildingBuffer = buildingManager.m_buildings.m_buffer;
            BuildingInfo      buildingInfo   = buildingBuffer[buildingID].Info;
            PrivateBuildingAI buildingAI     = buildingInfo?.GetAI() as PrivateBuildingAI;

            if (buildingInfo == null || buildingAI == null)
            {
                // If something went wrong, abort.
                Logging.Error("couldn't get existing building info");
                return;
            }

            // Check to see if this is historical or not, or is a RICO ploppable.
            bool isHistorical = buildingAI.IsHistorical(buildingID, ref Singleton <BuildingManager> .instance.m_buildings.m_buffer[buildingID], out bool _) || ModUtils.CheckRICOPloppable(buildingInfo);

            // Get target prefab (if needed, i.e. not historical or RICO ploppable).
            if (!isHistorical)
            {
                // Get upgrade/downgrade building target.
                targetInfo = GetTargetInfo(buildingID, targetLevel);
                if (targetInfo == null)
                {
                    // If we failed, don't do anything more.
                    return;
                }
            }

            // If we have a valid upgrade/downgrade target, proceed.
            if (isHistorical || targetInfo != null)
            {
                // Apply target level to our building and cancel all level-up progress.
                buildingBuffer[buildingID].m_level           = targetLevel;
                buildingBuffer[buildingID].m_levelUpProgress = 0;

                // Apply our upgrade/downgrade target if not historical
                if (!isHistorical)
                {
                    buildingManager.UpdateBuildingInfo(buildingID, targetInfo);
                }

                // Post-downgrade processing to update instance values - call game method if new level is equal to or greater than info base level, otherwise use custom method.
                BuildingInfo newInfo = targetInfo ?? buildingInfo;
                if (newInfo.GetAI() is PrivateBuildingAI newAI)
                {
                    if (targetLevel < (byte)newInfo.GetClassLevel())
                    {
                        // New level is less than info base level; call custom method.
                        CustomBuildingUpgraded(newAI, buildingID, ref buildingBuffer[buildingID]);
                    }
                    else
                    {
                        // New level is equal to or greater than info base level; call game method.
                        newAI.BuildingUpgraded(buildingID, ref buildingBuffer[buildingID]);
                    }
                }
            }
        }
        /// <summary>
        /// Render and show a preview of a building.
        /// </summary>
        /// <param name="building">The building to render</param>
        public void Show(BuildingInfo building)
        {
            // Update current selection to the new building.
            currentSelection = building;

            // Generate render if there's a selection with a mesh.
            if (currentSelection != null && currentSelection.m_mesh != null)
            {
                // Set default values.
                previewRender.CameraRotation = 210f;
                previewRender.Zoom           = 4f;

                // Set mesh and material for render.
                previewRender.SetTarget(currentSelection);

                // Set background.
                previewSprite.texture     = previewRender.Texture;
                noPreviewSprite.isVisible = false;

                // Render at next update.
                RenderPreview();
            }
            else
            {
                // No valid current selection with a mesh; reset background.
                previewSprite.texture     = null;
                noPreviewSprite.isVisible = true;
            }

            // Hide any empty building names.
            if (building == null)
            {
                buildingName.isVisible  = false;
                buildingLevel.isVisible = false;
                buildingSize.isVisible  = false;
            }
            else
            {
                // Set and show building name.
                buildingName.isVisible = true;
                buildingName.text      = UIBuildingDetails.GetDisplayName(currentSelection.name);
                UIUtils.TruncateLabel(buildingName, width - 45);
                buildingName.autoHeight = true;

                // Set and show building level.
                buildingLevel.isVisible = true;
                buildingLevel.text      = Translations.Translate("RPR_OPT_LVL") + " " + Mathf.Min((int)currentSelection.GetClassLevel() + 1, MaxLevelOf(currentSelection.GetSubService()));
                UIUtils.TruncateLabel(buildingLevel, width - 45);
                buildingLevel.autoHeight = true;

                // Set and show building size.
                buildingSize.isVisible = true;
                buildingSize.text      = currentSelection.GetWidth() + "x" + currentSelection.GetLength();
                UIUtils.TruncateLabel(buildingSize, width - 45);
                buildingSize.autoHeight = true;
            }
        }
        /// <summary>
        /// Perform and display volumetric calculations for the currently selected building.
        /// </summary>
        /// <param name="building">Selected building prefab</param>
        /// <param name="levelData">Population (level) calculation data to apply to calculations</param>
        /// <param name="floorData">Floor calculation data to apply to calculations</param>
        /// <param name="schoolData">School calculation data to apply to calculations</param>
        /// <param name="schoolData">Multiplier to apply to calculations</param>
        internal void CalculateVolumetric(BuildingInfo building, LevelData levelData, FloorDataPack floorData, SchoolDataPack schoolData, float multiplier)
        {
            // Safety first!
            if (building == null)
            {
                return;
            }

            // Reset message label.
            messageLabel.text = string.Empty;

            // Perform calculations.
            // Get floors and allocate area an number of floor labels.
            SortedList <int, float> floors = PopData.instance.VolumetricFloors(building.m_generatedInfo, floorData, out float totalArea);

            floorAreaLabel.text = totalArea.ToString("N0", LocaleManager.cultureInfo);
            numFloorsLabel.text = floors.Count.ToString();

            // Get total units.
            int totalUnits = PopData.instance.VolumetricPopulation(building.m_generatedInfo, levelData, floorData, multiplier, floors, totalArea);

            // Floor labels list.
            List <string> floorLabels = new List <string>();

            // What we call our units for this building.
            string unitName;

            switch (building.GetService())
            {
            case ItemClass.Service.Residential:
                // Residential - households.
                unitName = Translations.Translate("RPR_CAL_UNI_HOU");
                break;

            case ItemClass.Service.Education:
                // Education - students.
                unitName = Translations.Translate("RPR_CAL_UNI_STU");
                break;

            default:
                // Default - workplaces.
                unitName = Translations.Translate("RPR_CAL_UNI_WOR");
                break;
            }

            // See if we're using area calculations for numbers of units, i.e. areaPer is at least one.
            if (levelData.areaPer > 0)
            {
                // Determine area percentage to use for calculations (inverse of empty area percentage).
                float areaPercent = 1 - (levelData.emptyPercent / 100f);

                // Create new floor area labels by iterating through each floor.
                for (int i = 0; i < floors.Count; ++i)
                {
                    // StringBuilder, because we're doing a fair bit of manipulation here.
                    StringBuilder floorString = new StringBuilder("Floor ");

                    // Floor number
                    floorString.Append(i + 1);
                    floorString.Append(" " + Translations.Translate("RPR_CAL_VOL_ARA") + " ");
                    floorString.Append(floors[i].ToString("N0"));

                    // See if we're calculating units per individual floor.
                    if (!levelData.multiFloorUnits)
                    {
                        // Number of units on this floor - always rounded down.
                        int floorUnits = (int)((floors[i] * areaPercent) / levelData.areaPer);
                        // Adjust by multiplier (after rounded calculation above).
                        floorUnits = (int)(floorUnits * multiplier);

                        // Add extra info to label.
                        floorString.Append(" (");
                        floorString.Append(floorUnits.ToString("N0"));
                        floorString.Append(" ");
                        floorString.Append(unitName);
                        floorString.Append(")");
                    }

                    // Add new floor label item with results for this calculation.
                    floorLabels.Add(floorString.ToString());
                }
            }

            // Do we have a current school selection, and are we using school property overrides?
            if (schoolData != null && ModSettings.enableSchoolProperties)
            {
                // Yes - calculate and display school worker breakdown.
                int[] workers = SchoolData.instance.CalcWorkers(schoolData, totalUnits);
                schoolWorkerLabel.Show();
                schoolWorkerLabel.text = workers[0] + " / " + workers[1] + " / " + workers[2] + " / " + workers[3];

                // Calculate construction cost to display.
                int cost = SchoolData.instance.CalcCost(schoolData, totalUnits);
                ColossalFramework.Singleton <EconomyManager> .instance.m_EconomyWrapper.OnGetConstructionCost(ref cost, building.m_class.m_service, building.m_class.m_subService, building.m_class.m_level);

                // Calculate maintenance cost to display.
                int maintenance = SchoolData.instance.CalcMaint(schoolData, totalUnits) * 100;
                ColossalFramework.Singleton <EconomyManager> .instance.m_EconomyWrapper.OnGetMaintenanceCost(ref maintenance, building.m_class.m_service, building.m_class.m_subService, building.m_class.m_level);

                float displayMaint = Mathf.Abs(maintenance * 0.0016f);

                // And display school cost breakdown.
                costLabel.Show();
                costLabel.text = cost.ToString((!(displayMaint >= 10f)) ? Settings.moneyFormat : Settings.moneyFormatNoCents, LocaleManager.cultureInfo) + " / " + displayMaint.ToString((!(displayMaint >= 10f)) ? Settings.moneyFormat : Settings.moneyFormatNoCents, LocaleManager.cultureInfo);

                // Enforce school floors list position.
                ResetFloorListPosition();
            }
            else
            {
                // No - hide school worker breakdown and cost labels.
                schoolWorkerLabel.Hide();
                costLabel.Hide();

                // Enforce default floors list position.
                ResetFloorListPosition();
            }

            // Allocate our new list of labels to the floors list (via an interim fastlist to avoid race conditions if we 'build' manually directly into floorsList).
            FastList <object> fastList = new FastList <object>()
            {
                m_buffer = floorLabels.ToArray(),
                m_size   = floorLabels.Count
            };

            floorsList.rowsData = fastList;

            // Display total unit calculation result.
            switch (building.GetService())
            {
            case ItemClass.Service.Residential:
                // Residential building.
                totalJobsLabel.Hide();
                totalStudentsLabel.Hide();
                totalHomesLabel.Show();
                totalHomesLabel.text = totalUnits.ToString("N0", LocaleManager.cultureInfo);
                break;

            case ItemClass.Service.Education:
                // School building.
                totalHomesLabel.Hide();
                totalJobsLabel.Hide();
                totalStudentsLabel.Show();
                totalStudentsLabel.text = totalUnits.ToString("N0", LocaleManager.cultureInfo);
                break;

            default:
                // Workplace building.
                totalHomesLabel.Hide();
                totalStudentsLabel.Hide();
                totalJobsLabel.Show();
                totalJobsLabel.text = totalUnits.ToString("N0", LocaleManager.cultureInfo);
                break;
            }

            // Display commercial visit count, or hide the label if not commercial.
            if (building.GetAI() is CommercialBuildingAI)
            {
                visitCountLabel.Show();
                visitCountLabel.text = RealisticVisitplaceCount.PreviewVisitCount(building, totalUnits).ToString();
            }
            else
            {
                visitCountLabel.Hide();
            }

            // Display production count, or hide the label if not a production building.
            if (building.GetAI() is PrivateBuildingAI privateAI && (privateAI is OfficeBuildingAI || privateAI is IndustrialBuildingAI || privateAI is IndustrialExtractorAI))
            {
                productionLabel.Show();
                productionLabel.text = privateAI.CalculateProductionCapacity(building.GetClassLevel(), new ColossalFramework.Math.Randomizer(), building.GetWidth(), building.GetLength()).ToString();
            }
        /// <summary>
        /// Called whenever the currently selected building is changed to update the panel display.
        /// </summary>
        /// <param name="building"></param>
        public void SelectionChanged(BuildingInfo building)
        {
            if ((building == null) || (building.name == null))
            {
                // If no valid building selected, then hide the calculations panel.
                detailsPanel.height    = 0;
                detailsPanel.isVisible = false;
                return;
            }

            // Variables to compare actual counts vs. mod count, to see if there's another mod overriding counts.
            int appliedCount;
            int modCount;

            // Building model size, not plot size.
            Vector3 buildingSize = building.m_size;
            int     floorCount;

            // Array used for calculations depending on building service/subservice (via DataStore).
            int[] array;
            // Default minimum number of homes or jobs is one; different service types will override this.
            int minHomesJobs = 1;
            int customHomeJobs;

            // Check for valid building AI.
            if (!(building.GetAI() is PrivateBuildingAI buildingAI))
            {
                Debugging.Message("invalid building AI type in building details");
                return;
            }

            // Residential vs. workplace AI.
            if (buildingAI is ResidentialBuildingAI)
            {
                // Get appropriate calculation array.
                array = ResidentialBuildingAIMod.GetArray(building, (int)building.GetClassLevel());

                // Set calculated homes label.
                homesJobsCalcLabel.text = Translations.Translate("RPR_CAL_HOM_CALC");

                // Set customised homes label and get value (if any).
                homesJobsCustomLabel.text = Translations.Translate("RPR_CAL_HOM_CUST");
                customHomeJobs            = ExternalCalls.GetResidential(building);

                // Applied homes is what's actually being returned by the CaclulateHomeCount call to this building AI.
                // It differs from calculated homes if there's an override value for that building with this mod, or if another mod is overriding.
                appliedCount = buildingAI.CalculateHomeCount(building.GetClassLevel(), new Randomizer(0), building.GetWidth(), building.GetLength());
                homesJobsActualLabel.text = Translations.Translate("RPR_CAL_HOM_APPL") + appliedCount;
            }
            else
            {
                // Workplace AI.
                // Default minimum number of jobs is 4.
                minHomesJobs = 4;

                // Find the correct array for the relevant building AI.
                switch (building.GetService())
                {
                case ItemClass.Service.Commercial:
                    array = CommercialBuildingAIMod.GetArray(building, (int)building.GetClassLevel());
                    break;

                case ItemClass.Service.Office:
                    array = OfficeBuildingAIMod.GetArray(building, (int)building.GetClassLevel());
                    break;

                case ItemClass.Service.Industrial:
                    if (buildingAI is IndustrialExtractorAI)
                    {
                        array = IndustrialExtractorAIMod.GetArray(building, (int)building.GetClassLevel());
                    }
                    else
                    {
                        array = IndustrialBuildingAIMod.GetArray(building, (int)building.GetClassLevel());
                    }
                    break;

                default:
                    Debugging.Message("invalid building service in building details");
                    return;
                }

                // Set calculated jobs label.
                homesJobsCalcLabel.text = Translations.Translate("RPR_CAL_JOB_CALC") + " ";

                // Set customised jobs label and get value (if any).
                homesJobsCustomLabel.text = Translations.Translate("RPR_CAL_JOB_CUST") + " ";
                customHomeJobs            = ExternalCalls.GetWorker(building);

                // Applied jobs is what's actually being returned by the CalculateWorkplaceCount call to this building AI.
                // It differs from calculated jobs if there's an override value for that building with this mod, or if another mod is overriding.
                int[] jobs = new int[4];
                buildingAI.CalculateWorkplaceCount(building.GetClassLevel(), new Randomizer(0), building.GetWidth(), building.GetLength(), out jobs[0], out jobs[1], out jobs[2], out jobs[3]);
                appliedCount = jobs[0] + jobs[1] + jobs[2] + jobs[3];
                homesJobsActualLabel.text = Translations.Translate("RPR_CAL_JOB_APPL") + " " + appliedCount;
            }

            // Reproduce CalcBase calculations to get building area.
            int calcWidth  = building.GetWidth();
            int calcLength = building.GetLength();

            floorCount = Mathf.Max(1, Mathf.FloorToInt(buildingSize.y / array[DataStore.LEVEL_HEIGHT]));

            // If CALC_METHOD is zero, then calculations are based on building model size, not plot size.
            if (array[DataStore.CALC_METHOD] == 0)
            {
                // If asset has small x dimension, then use plot width in squares x 6m (75% of standard width) instead.
                if (buildingSize.x <= 1)
                {
                    calcWidth *= 6;
                }
                else
                {
                    calcWidth = (int)buildingSize.x;
                }

                // If asset has small z dimension, then use plot length in squares x 6m (75% of standard length) instead.
                if (buildingSize.z <= 1)
                {
                    calcLength *= 6;
                }
                else
                {
                    calcLength = (int)buildingSize.z;
                }
            }
            else
            {
                // If CALC_METHOD is nonzero, then caluclations are based on plot size, not building size.
                // Plot size is 8 metres per square.
                calcWidth  *= 8;
                calcLength *= 8;
            }

            // Display calculated (and retrieved) details.
            detailLabels[(int)Details.width].text       = Translations.Translate("RPR_CAL_BLD_X") + " " + calcWidth;
            detailLabels[(int)Details.length].text      = Translations.Translate("RPR_CAL_BLD_Z") + " " + calcLength;
            detailLabels[(int)Details.height].text      = Translations.Translate("RPR_CAL_BLD_Y") + " " + (int)buildingSize.y;
            detailLabels[(int)Details.personArea].text  = Translations.Translate("RPR_CAL_BLD_M2") + " " + array[DataStore.PEOPLE];
            detailLabels[(int)Details.floorHeight].text = Translations.Translate("RPR_CAL_FLR_Y") + " " + array[DataStore.LEVEL_HEIGHT];
            detailLabels[(int)Details.floors].text      = Translations.Translate("RPR_CAL_FLR") + " " + floorCount;

            // Area calculation - will need this later.
            int calculatedArea = calcWidth * calcLength;

            detailLabels[(int)Details.area].text = Translations.Translate("RPR_CAL_M2") + " " + calculatedArea;

            // Show or hide extra floor modifier as appropriate (hide for zero or less, otherwise show).
            if (array[DataStore.DENSIFICATION] > 0)
            {
                detailLabels[(int)Details.extraFloors].text      = Translations.Translate("RPR_CAL_FLR_M") + " " + array[DataStore.DENSIFICATION];
                detailLabels[(int)Details.extraFloors].isVisible = true;
            }
            else
            {
                detailLabels[(int)Details.extraFloors].isVisible = false;
            }

            // Set minimum residences for high density.
            if ((building.GetSubService() == ItemClass.SubService.ResidentialHigh) || (building.GetSubService() == ItemClass.SubService.ResidentialHighEco))
            {
                // Minimum of 2, or 90% number of floors, whichever is greater. This helps the 1x1 high density.
                minHomesJobs = Mathf.Max(2, Mathf.CeilToInt(0.9f * floorCount));
            }

            // Perform actual household or workplace calculation.
            modCount = Mathf.Max(minHomesJobs, (calculatedArea * (floorCount + Mathf.Max(0, array[DataStore.DENSIFICATION]))) / array[DataStore.PEOPLE]);
            homesJobsCalcLabel.text += modCount;

            // Set customised homes/jobs label (leave blank if no custom setting retrieved).
            if (customHomeJobs > 0)
            {
                homesJobsCustomLabel.text += customHomeJobs.ToString();

                // Update modCount to reflect the custom figures.
                modCount = customHomeJobs;
            }

            // Check to see if Ploppable RICO Revisited is controlling this building's population.
            if (ModUtils.CheckRICO(building))
            {
                messageLabel.text = Translations.Translate("RPR_CAL_RICO");
                messageLabel.Show();
            }
            else
            {
                // Hide message text by default.
                messageLabel.Hide();
            }

            // We've got a valid building and results, so show panel.
            detailsPanel.height    = 270;
            detailsPanel.isVisible = true;
        }
        /// <summary>
        /// Called whenever the currently selected building is changed to update the panel display.
        /// </summary>
        /// <param name="building">Newly selected building</param>
        public void SelectionChanged(BuildingInfo building)
        {
            // Make sure we have a valid selection before proceeding.
            if (building?.name == null)
            {
                return;
            }

            // Variables to compare actual counts vs. mod count, to see if there's another mod overriding counts.
            int appliedCount;
            int modCount;

            // Building model size, not plot size.
            Vector3 buildingSize = building.m_size;
            int     floorCount;

            // Array used for calculations depending on building service/subservice (via DataStore).
            int[] array;
            // Default minimum number of homes or jobs is one; different service types will override this.
            int minHomesJobs = 1;
            int customHomeJobs;

            // Check for valid building AI.
            if (!(building.GetAI() is PrivateBuildingAI buildingAI))
            {
                Logging.Error("invalid building AI type in building details for building ", building.name);
                return;
            }

            // Residential vs. workplace AI.
            if (buildingAI is ResidentialBuildingAI)
            {
                // Get appropriate calculation array.
                array = LegacyAIUtils.GetResidentialArray(building, (int)building.GetClassLevel());

                // Set calculated homes label.
                homesJobsCalcLabel.text = Translations.Translate("RPR_CAL_HOM_CALC");

                // Set customised homes label and get value (if any).
                homesJobsCustomLabel.text = Translations.Translate("RPR_CAL_HOM_CUST");
                customHomeJobs            = OverrideUtils.GetResidential(building);

                // Applied homes is what's actually being returned by the CaclulateHomeCount call to this building AI.
                // It differs from calculated homes if there's an override value for that building with this mod, or if another mod is overriding.
                appliedCount = buildingAI.CalculateHomeCount(building.GetClassLevel(), new Randomizer(0), building.GetWidth(), building.GetLength());
                homesJobsActualLabel.text = Translations.Translate("RPR_CAL_HOM_APPL") + appliedCount;
            }
            else
            {
                // Workplace AI.
                // Default minimum number of jobs is 4.
                minHomesJobs = 4;

                // Find the correct array for the relevant building AI.
                switch (building.GetService())
                {
                case ItemClass.Service.Commercial:
                    array = LegacyAIUtils.GetCommercialArray(building, (int)building.GetClassLevel());
                    break;

                case ItemClass.Service.Office:
                    array = LegacyAIUtils.GetOfficeArray(building, (int)building.GetClassLevel());
                    break;

                case ItemClass.Service.Industrial:
                    if (buildingAI is IndustrialExtractorAI)
                    {
                        array = LegacyAIUtils.GetExtractorArray(building);
                    }
                    else
                    {
                        array = LegacyAIUtils.GetIndustryArray(building, (int)building.GetClassLevel());
                    }
                    break;

                default:
                    Logging.Error("invalid building service in building details for building ", building.name);
                    return;
                }

                // Set calculated jobs label.
                homesJobsCalcLabel.text = Translations.Translate("RPR_CAL_JOB_CALC") + " ";

                // Set customised jobs label and get value (if any).
                homesJobsCustomLabel.text = Translations.Translate("RPR_CAL_JOB_CUST") + " ";
                customHomeJobs            = OverrideUtils.GetWorker(building);

                // Applied jobs is what's actually being returned by the CalculateWorkplaceCount call to this building AI.
                // It differs from calculated jobs if there's an override value for that building with this mod, or if another mod is overriding.
                int[] jobs = new int[4];
                buildingAI.CalculateWorkplaceCount(building.GetClassLevel(), new Randomizer(0), building.GetWidth(), building.GetLength(), out jobs[0], out jobs[1], out jobs[2], out jobs[3]);
                appliedCount = jobs[0] + jobs[1] + jobs[2] + jobs[3];
                homesJobsActualLabel.text = Translations.Translate("RPR_CAL_JOB_APPL") + " " + appliedCount;

                // Show visitor count for commercial buildings.
                if (buildingAI is CommercialBuildingAI commercialAI)
                {
                    visitCountLabel.Show();
                    visitCountLabel.text = Translations.Translate("RPR_CAL_VOL_VIS") + " " + commercialAI.CalculateVisitplaceCount(building.GetClassLevel(), new Randomizer(), building.GetWidth(), building.GetLength());
                }
                else
                {
                    visitCountLabel.Hide();
                }

                // Display production count, or hide the label if not a production building.
                if (building.GetAI() is PrivateBuildingAI privateAI && (privateAI is OfficeBuildingAI || privateAI is IndustrialBuildingAI || privateAI is IndustrialExtractorAI))
                {
                    productionLabel.Show();
                    productionLabel.text = Translations.Translate("RPR_CAL_VOL_PRD") + " " + privateAI.CalculateProductionCapacity(building.GetClassLevel(), new ColossalFramework.Math.Randomizer(), building.GetWidth(), building.GetLength()).ToString();
                }