Writing a mod for Cities Skylines (2)

Writing a mod for Cities Skylines (2)

Last week I wrote about the small mod I wrote for Cities Skylines and how I added the enable/disable all button to an emergency shelter’s information panel. Today I’m going to elaborate on this topic and explain what the logic behind this button is, and where I found this information.

First we need to summarize what the button should do, and what it needs to do so. The button needs to find all shelters in the current map, and turn them on or off. It is possible that some shelters are enabled, while others are disabled. I’ve decided that the state of the selected shelter accounts for whether we should enable, or respectively disable all other shelters, depends on the state of the selected shelter. When the user selects a shelter that’s currently disabled, the button can be used to enable all shelters, and when he chooses an enabled shelter, the button is used to disable all shelters.

Iterate over all shelters

From the previous paragraph, it is clear that we need a way to determine the state of the shelter associated with the opened information panel. After taking a look at the source code of ShelterWorldInfoPanel with ILSpy, I found a public property isCityServiceEnabled that can be used to get the current state of the selected shelter.

Next, we need a way of enumerating all shelters that are placed in the city. I found this piece of code on Reddit, which demonstrates how to iterate over all buildings of a certain service type:

// assemblies needed:
Assembly-CSharp
ColossalManaged
ICities
UnityEngine

// namespaces needed:
using ColossalFramework;
using ColossalFramework.Plugins;
using ICities;

var counts = new Dictionary<string, int>();
var buildingManager = Singleton<BuildingManager>.instance;
foreach (var s in new[]
{
	ItemClass.Service.Road,
	ItemClass.Service.Electricity,
	ItemClass.Service.Water,
	// ...
	ItemClass.Service.PublicTransport,
})
{
	var serviceBuildings = buildingManager.GetServiceBuildings(s);
	if (serviceBuildings.m_buffer == null || serviceBuildings.m_size > serviceBuildings.m_buffer.Length)
		continue;

	for (var index = 0; index < serviceBuildings.m_size; ++index)
	{
		var id = serviceBuildings.m_buffer[index];
		var info = buildingManager.m_buildings.m_buffer[id].Info;
		var buildingName = info.m_buildingAI.name;
	}
}

After some slight modifications, and finding out that the service type for shelters is ItemClass.Service.Disaster I came to the next code snippet. The Disaster service is used for multiple types of buildings. That’s why I need to add an extra filter if (info.m_buildingAI.GetType() == typeof(ShelterAI)) where I check that the current building is indeed a shelter:

var buildingManager = Singleton<BuildingManager>.instance;

if (!Singleton<BuildingManager>.exists) {
	return;
}

var serviceBuildings = buildingManager.GetServiceBuildings(ItemClass.Service.Disaster);

if (serviceBuildings.m_buffer == null || serviceBuildings.m_size > serviceBuildings.m_buffer.Length) {
	return;
}

for (var index = 0; index < serviceBuildings.m_size; ++index) {
	var id = serviceBuildings.m_buffer[index];
	var building = buildingManager.m_buildings.m_buffer[id];
	var info = building.Info;

	if (info.m_buildingAI.GetType() == typeof(ShelterAI)) {
		// Switch building status
	}
}

Change building status

Turning a building on or off is not as simple as changing some on boolean somewhere in the code. It took some time for me to realise that the ShelterWorldInfoPanel already provides this functionality and that I should investigate the code in there. It turns out that a function OnOnOffCheck() is called whenever the player clicks the on/off checkbox in the upper left corner of this information panel. This function sets the local isCityServiceEnabled property from true to false, or the other way around. The isCityServiceEnabled property consists of both a getter and setter that seem to be looking at a value, conveniently named m_productionRate to determine the current buildings status. A value of 100 stands for a building that’s currently enabled, while a value of 0 indicates a disabled entity. We thus need to switch this value for all shelters found in the map, which leads to the following block of code:

button.eventClicked += (component, state) => {
	var buildingManager = Singleton<BuildingManager>.instance;

	if (!Singleton<BuildingManager>.exists) {
		return;
	}

	var serviceBuildings = buildingManager.GetServiceBuildings(ItemClass.Service.Disaster);

	if (serviceBuildings.m_buffer == null || serviceBuildings.m_size > serviceBuildings.m_buffer.Length) {
		return;
	}

	for (var index = 0; index < serviceBuildings.m_size; ++index) {
		var id = serviceBuildings.m_buffer[index];
		var building = buildingManager.m_buildings.m_buffer[id];
		var info = building.Info;

		if (info.m_buildingAI.GetType() == typeof(ShelterAI)) {
			Singleton<SimulationManager>.instance.AddAction(ToggleBuilding(id, !IsBuildingEnabled(building)));
		}
	}
};

Note that this code is called every time the enable/disable button is clicked. The on/off checkbox looks at the isCityServiceEnabled value to determine it’s state, but does not update directly, we need to change it’s internal state after clicking changing a buildings state somehow.

Update on/off checkbox after switching state

According to this tutorial we should create a new class that extends the ThreadingExtensionBase class and that sets the desired state of the on/off checkbox in the OnUpdate() method. We simply need to check the status of the building associated with the ShelterWorldInfoPanel and set the state of the on/off checkbox accordingly. This checkbox is conveniently called On/Off and can be found using Unity’s Find<T>() method. Note that the code inside off OnUpdate() is called at every update of the simulation (multiple times per second), making it absolutely necessary to keep this code as lightweight as possible. This could otherwise drastically impact the performance of the game.

using ICities;
using UnityEngine;

namespace CSBatchBuildingEnabler
{
    public class CSBatchBuildingEnablerPanelMonitor: ThreadingExtensionBase {
        private ShelterWorldInfoPanel panel;
        private UICheckBox onOffCheckBox;
       

        public override void OnUpdate(float realTimeDelta, float simulationTimeDelta) {
            if (!FindComponents()) {
                return;
            }

            if (panel.component.isVisible) {
                ushort buildingId = ShelterWorldInfoPanel.GetCurrentInstanceID().Building;
                
                // display the right checkbox state  
                onOffCheckBox.isChecked = Singleton<BuildingManager>.instance.m_buildings.m_buffer[buildingId].m_productionRate != 0; 
            }
        }

        public bool FindComponents() {
            if(panel != null && onOffCheckBox != null) return true;

            panel = UIView.library.Get<ShelterWorldInfoPanel>(typeof(ShelterWorldInfoPanel).Name);
            if (panel == null) return false;
            
           
            onOffCheckBox = panel.component.Find<UICheckBox>("On/Off");
            return onOffCheckBox != null;
        }
    }
}

That’s it! I’m done explaining what my mod looks like. Thanks for following up to this point! Feel free to ask any questions in the comments section below. I’ll try to answer them as often as possible.

Leave a Reply

Your email address will not be published. Required fields are marked *