In this tutorial, we will create a simple mod that globally replaces vanilla road trees with custom trees made by MrMaison. Like in Tutorial 1, we will use a loading hook, prefab collections and the ModTools scene explorer. You will also learn how to add settings to your mod.
Step 1: Exploring and modifying network and tree prefabs with the ModTools scene explorer
Before writing the actual mod, we will apply the replacement with the ModTools scene explorer.
Just like buildings, networks and trees are defined as prefabs (NetInfo and TreeInfo, see Tutorial 1 Step 3). There are various way to find these prefabs in the ModTools scene explorer. The easiest way that works for vanilla and workshop assets is the ToolsController. "Tools" in Cities: Skylines allow you to interact with the game world. The most obvious tool is the bulldozer, but there are also tools for the placement of objects, zoning, transport line creation and camera control. For this tutorial, we will access the TreeTool and the NetTool, which hold a reference to the tree/network prefab you are placing.
Select a tree you in the Landscaping panel. I chose the Royal Palm made by MrMaison. While the placement mode is enabled, open the ModTools scene explorer (CTRL + E). On the left, select Tool Controller > TreeTool. You will see the properties of the TreeTool on the right. The currently selected tree prefab is stored in the m_prefab property. Press the "Copy" button to copy the reference of the tree prefab to the ModTools clipboard (it does not copy the actual data, just the memory address of the prefab).
Tip: For every prefab (prop, building, network, ...) there is also a "Preview" button that displays a model viewer, and more importantly, a "Plop" button that allows you to plop the asset. With this button, you can place assets which are not available in the panels of the game, like props or sub-buildings.
Now, select a road with trees in the Roads panel. I chose the "Large Avenue with Grass" that was added by the MT update. Open the scene explorer and select Tool Controller > NetTool. On the right, expand the m_prefab property (the currently selected network prefab).
You can now see the properties of the network prefab. The structure of a network prefab looks like this:
NetInfo: Network Prefab
├ m_nodes[]: Array of objects defining meshes/textures for intersections
├ m_segments[]: Array of objects defining meshes/textures for segments
├ m_lanes[]: Array of NetInfo.Lane objects
│ └ NetInfo.Lane: Defines a lane used by vehicles or pedestrians + prop/tree decoration
│ ├ m_laneProps
│ │ └ m_props[]: Array of prop items
│ │ └ NetLaneProps.Prop: Like BuildingInfo.Prop, defines the position a prop/tree
│ │ ├ m_prop/m_finalProp: The prop prefab
│ │ ├ m_tree/m_finalTree: The tree prefab
│ │ ├ m_probability: Probability that the prop/tree spawns
│ │ ├ m_repeatDistance: Distance between two props/trees
│ │ └ ...
│ └ ...
└ ...
Expand the m_lanes array and search for the lane that contains the tree prop items you want to replace. In case of the "Large Avenue with Grass", navigate to m_lanes > m_lanes.[0] > m_laneProps > m_props > m_props.[4]:
Click the "Paste" button on the right for the properties m_finalTree and m_tree. This will assign the custom tree prefab we copied earlier to the prop item:
You will instantly see the result: The trees of all avenues in the city have been replaced.
In the next steps, we will create a mod that automates the replacement.
Step 2: Project Setup
Create a new project named "RoadTreeReplacer" in VS2017, following Method 2 in Tutorial 0 (working with a text editor is still possible in this tutorial, but not recommended)
Your IUserMod implementation should look like this:
using ICities;
namespace RoadTreeReplacer
{
public class RoadTreeReplacerMod : IUserMod
{
public string Name => "Road Tree Replacer";
public string Description => "Replaces the boring Oak roadside trees with MrMaison's creations";
}
}
Step 3: Loading Hook & Tree Replacement Logic
Create a new file called RoadTreeReplacerLoading.cs in the Solution Explorer. This file will contain our ILoadingExtension implementation that is invoked by the game when a save is loaded (See Tutorial 1 Step 2). Like in the last tutorial, we will use the static PrefabCollection class to find our network and tree prefab.
Add the following contents to the file:
using UnityEngine;
using ICities;
namespace RoadTreeReplacer
{
public class RoadTreeReplacerLoading : LoadingExtensionBase
{
public override void OnLevelLoaded(LoadMode mode)
{
// Find the network
NetInfo netPrefab = PrefabCollection<NetInfo>.FindLoaded("Avenue Large With Grass");
// Find the tree we want to use as a replacement
TreeInfo treePrefab = PrefabCollection<TreeInfo>.FindLoaded("909448182.Royal Palm_Data");
// Check if both prefabs are loaded, cancel if not
if (netPrefab == null)
{
Debug.LogError("RTR: The network could not be found");
return;
}
if (treePrefab == null)
{
Debug.LogError("RTR: The replacement tree could not be found");
return;
}
// cancel if lanes array is null (networks without lanes)
if (netPrefab.m_lanes == null) return;
// iterate through all lanes
foreach (NetInfo.Lane lane in netPrefab.m_lanes)
{
// cancel if lane props array is null (networks without lanes)
if (lane?.m_laneProps?.m_props == null) continue;
// iterate through all lane props of that lane
foreach (NetLaneProps.Prop laneProp in lane.m_laneProps.m_props)
{
if (laneProp == null) continue;
// if the tree/finalTree field is set, replace it with our tree prefab
if (laneProp.m_tree != null)
{
laneProp.m_tree = treePrefab;
}
if (laneProp.m_finalTree != null)
{
laneProp.m_finalTree = treePrefab;
}
}
}
Debug.Log("RTR: Replacement successful!");
}
}
}
Tip: You can use the Debug.Log(...) method to add log entries to the output_log.txt. The log entries are also displayed in the ModTools console, which can be used for debugging your mod.
Now compile the mod (F6). If there are any compilation errors, use the error list in VS2017 to locate and fix the error (View > Error List)
When the compilation was successful, run the game. Enable the mod in content manager (if your mod does not show up, you probably forgot to setup the post build script).
Now create or load a city. The tree replacement is now automated. You will also see the success message in the ModTools console:
Before we proceed with the next step, we will move our replacement code to a separate ReplaceNetTrees method. We can call this method multiple times with a single line of code:
using UnityEngine;
using ICities;
namespace RoadTreeReplacer
{
public class RoadTreeReplacerLoading : LoadingExtensionBase
{
public override void OnLevelLoaded(LoadMode mode)
{
ReplaceNetTrees("Avenue Large With Grass", "909448182.Royal Palm_Data");
ReplaceNetTrees("Medium Road Decoration Trees", "909448182.Royal Palm_Data");
}
private void ReplaceNetTrees(string netName, string treeName)
{
NetInfo netPrefab = PrefabCollection<NetInfo>.FindLoaded(netName);
TreeInfo treePrefab = PrefabCollection<TreeInfo>.FindLoaded(treeName);
if (netPrefab == null)
{
Debug.LogError($"RTR: The network {netName} could not be found");
return;
}
if (treePrefab == null)
{
Debug.LogError($"RTR: The replacement tree {treeName} could not be found");
return;
}
if (netPrefab.m_lanes == null) return;
foreach (NetInfo.Lane lane in netPrefab.m_lanes)
{
if (lane?.m_laneProps?.m_props == null) continue;
foreach (NetLaneProps.Prop laneProp in lane.m_laneProps.m_props)
{
if (laneProp == null) continue;
if (laneProp.m_tree != null) laneProp.m_tree = treePrefab;
if (laneProp.m_finalTree != null) laneProp.m_finalTree = treePrefab;
}
}
Debug.Log($"RTR: Replacement of tree in network {netName} successful!");
}
}
}
Step 4: Adding Mod Settings
Right now the functionality of the mod is fixed, there is no way to configure which trees are replaced. Adding settings to a mod is a difficult task. It will require 3 components:
User interface with checkboxes or dropdown menus (using the settings API provided by CO, or a custom window)
Data structure for the settings data (usually a C# class, or a set of key-value pairs)
Serialization System (to .xml file, or save game)
To keep it simple, we will only add a simple settings page with 3 dropdown options (Small Road Tree, Medium Road Tree, Large Road Tree), with a fixed number of trees to choose from (Default, Royal Palm, Weeping Silver Birch, River Red Gum Small). The settings will saved in a .xml file in the Cities: Skylines installation directory. The settings are global, that means the mod applies the same settings to all cities (in one of the next tutorials, I will show you how to save additional data in the save game).
Serialization System
Add a new file called Configuration.cs and paste this code.
It is a very minimalistic serialization library that I've written some time ago. It does all the heavy lifting for you (loading and saving of .xml files, transformation from/to C# objects). It is not important to understand what happens internally, you just have to understand how to use it.
The library provides a method to load your configuration data:
YourConfiguration config = Configuration<YourConfiguration>.Load();
And to save it:
Configuration<YourConfiguration>.Save();
You only have to provide the data class that defines the structure of the .xml file.
Note: If you are using a Mac, you may have to add the System.Xml dependency to your project
Data Structure
Add a new class named RoadTreeReplacerConfiguration. This data class contains the string options we want to save:
namespace RoadTreeReplacer
{
[ConfigurationPath("RoadTreeReplacer.xml")]
public class RoadTreeReplacerConfiguration
{
public string SmallRoadTree { get; set; } = "909448182.Royal Palm_Data";
public string MediumRoadTree { get; set; } = "909448182.Royal Palm_Data";
public string LargeRoadTree { get; set; } = "909448182.Royal Palm_Data";
}
}
The [ConfigurationPath] attribute is read by the serialization library. The strings on the right are the default values used when a new configuration is created.
User Interface
Some time ago, CO added a simple settings API that allows you to create simple setting menus with a few lines of code. To use it, add a new method called OnSettingsUI to your IUserMod implementation:
using System;
using System.Collections.Generic;
using ICities;
namespace RoadTreeReplacer
{
public class RoadTreeReplacerMod : IUserMod
{
public string Name => "Road Tree Replacer";
public string Description => "Replaces the boring Oak roadside trees with MrMaison's creations";
// The strings displayed in the dropdown
private static readonly string[] OptionLabels =
{
"Default",
"Royal Palm",
"Weeping Silver Birch",
"River Red Gum"
};
// The corresponding prefab names
private static readonly string[] OptionValues =
{
"Tree2Variant",
"909448182.Royal Palm_Data",
"765126845.Weeping Silver Birch_Data",
"742114726.River Red Gum small_Data"
};
// Sets up a settings user interface
public void OnSettingsUI(UIHelperBase helper)
{
// Load the configuration
RoadTreeReplacerConfiguration config = Configuration<RoadTreeReplacerConfiguration>.Load();
// Small Roads
int smallSelectedIndex = GetSelectedOptionIndex(config.SmallRoadTree);
helper.AddDropdown("Small Road Tree", OptionLabels, smallSelectedIndex, sel =>
{
// Change config value and save config
config.SmallRoadTree = OptionValues[sel];
Configuration<RoadTreeReplacerConfiguration>.Save();
});
// Medium Roads
int mediumSelectedIndex = GetSelectedOptionIndex(config.MediumRoadTree);
helper.AddDropdown("Medium Road Tree", OptionLabels, mediumSelectedIndex, sel =>
{
// Change config value and save config
config.MediumRoadTree = OptionValues[sel];
Configuration<RoadTreeReplacerConfiguration>.Save();
});
// Large Roads
int largeSelectedIndex = GetSelectedOptionIndex(config.LargeRoadTree);
helper.AddDropdown("Large Road Tree", OptionLabels, largeSelectedIndex, sel =>
{
// Change config value and save config
config.LargeRoadTree = OptionValues[sel];
Configuration<RoadTreeReplacerConfiguration>.Save();
});
}
// Returns the index number of the option that is currently selected
private int GetSelectedOptionIndex(string value)
{
int index = Array.IndexOf(OptionValues, value);
if (index < 0) index = 0;
return index;
}
}
}
What happens here?
Load the config data with the Load method provided by the library
Get the index of the dropdown option that should be selected at first
Add a dropdown option with label, options, selected index and a callback (called when selection is changed)
(Repeat 2. and 3. for medium and large roads)
The OptionLabels array contains the strings displayed in the dropdown menus. The other array, OptionValues, contains the internal names of the prefabs.
The callback is a lamda function that takes the selected index (0-3). It saves the value that corresponds to the selected index in the configuration file.
The game automatically creates a settings page for mods which are implementing the OnSettingsUI method. No further steps are needed.
Tip: Other methods for UI element generation: AddButton(), AddCheckbox(), AddGroup(), AddSlider(), AddSpace(), AddTextfield()
Step 5: Using the configuration values in our loading hook
In the RoadTreeReplacerLoading class, replace the OnLevelLoaded method:
public override void OnLevelLoaded(LoadMode mode)
{
// Load the configuration
RoadTreeReplacerConfiguration config = Configuration<RoadTreeReplacerConfiguration>.Load();
ReplaceNetTrees("Basic Road Decoration Trees", config.SmallRoadTree);
ReplaceNetTrees("Oneway Road Decoration Trees", config.SmallRoadTree);
ReplaceNetTrees("Medium Road Decoration Trees", config.MediumRoadTree);
ReplaceNetTrees("Avenue Large With Grass", config.MediumRoadTree);
ReplaceNetTrees("Avenue Large With Buslanes Grass", config.MediumRoadTree);
ReplaceNetTrees("Large Road Decoration Trees", config.LargeRoadTree);
ReplaceNetTrees("Large Oneway Decoration Trees", config.LargeRoadTree);
}
This will replace the trees of all vanilla roads with the trees selected in the settings. And we are done!
After changing the options for the first time, the settings are saved to .xml:
Note: To apply the changed settings, you have to reload your city.
Happy Coding!
Download Source
Next part: