Jump to content

7 posts in this topic Last Reply

Highlighted Posts

Posted:
Last Online:  
 

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).

PXndc7n.jpg

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).

d36v5WU.jpg

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]:

7QBJRqZ.png

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:

ROzV6mL.png

You will instantly see the result: The trees of all avenues in the city have been replaced.

Xmfr9N5.jpg

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.

yq17cOv.png

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:

ANeAPFY.jpg

 

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?

  1. Load the config data with the Load method provided by the library
  2. Get the index of the dropdown option that should be selected at first
  3. Add a dropdown option with label, options, selected index and a callback (called when selection is changed)
  4. (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!

QJ4orWT.pngxljgdQs.jpg

After changing the options for the first time, the settings are saved to .xml:

AoHSCYP.png

Note: To apply the changed settings, you have to reload your city.

Happy Coding!

Download Source

 

Next part:

 

  • Like 5

Share this post


Link to post
Share on other sites
Posted:
Last Online:  
 

As usual, perfect turotial.

Maybe some tutorial on game UI, creation, modification?

Share this post


Link to post
Share on other sites
  • Original Poster
  • Posted:
    Last Online:  
     
    1 hour ago, noob_vl said:

    As usual, perfect turotial.

    Maybe some tutorial on game UI, creation, modification?

    Take a look at the latest tutorial:

     

    Share this post


    Link to post
    Share on other sites
    Posted:
    Last Online:  
     

    Not sure if this qualifies as a necropost or not (if so, sorry), but on mac, System.Xml is not added as a reference automatically, so Configuration.cs throws an "error". You'll need to add it by right-clicking on the References folder, then clicking "Edit References", and searching it.


    Hello! Salut! Привет! | My Workshop (follow me? :]) • My Github • My Twitter

     

    Share this post


    Link to post
    Share on other sites
  • Original Poster
  • Posted:
    Last Online:  
     
    7 hours ago, BlazingBoldly said:

    Not sure if this qualifies as a necropost or not (if so, sorry), but on mac, System.Xml is not added as a reference automatically, so Configuration.cs throws an "error". You'll need to add it by right-clicking on the References folder, then clicking "Edit References", and searching it.

    It's alright, the tutorial is still relevant. Thanks for your report, I will add a small note.

    I'm actually happy to see a reply, seems like the tutorial is of use for some people :)

     

    Share this post


    Link to post
    Share on other sites
    Posted:
    Last Online:  
     

    Hi @boformer,

    I found your tutorial series through this site, and attempted to go through it. But I ran into some problems in this tutorial. Specifically,

    • I couldn't paste the tree to the road in the first step, as the paste option wasn't present.
    • And in the 3rd step, the mod caused an out of date version error.

    What I`m not sure is whether I am doing something incorrectly or if the game has changed so some specifics are now out of date.

    Sorry to bring this back from the dead again. I hope it's not annoying to have this come up again.

    Share this post


    Link to post
    Share on other sites

    Sign In or register to comment...

    To comment in reply, you must be a community member

    Sign In  

    Already have an account? Sign in here.

    Sign In Now

    Create an Account  

    Sign up to join our friendly community. It's easy!  

    Register a New Account


    ×

    Thank You for the Continued Support!

    Simtropolis depends on donations to fund site maintenance costs.
    Without your support, we just would not be in our 24th year online!  You really help make this a great community. *:thumb:

    But we still need your support to stay online. If you're able to, please consider a donation to help us stay up and running. This helps sustain a platform where we can share our community creations for years to come.

    Make a Donation, Get a Gift!

    Expand your city with the best from the Simtropolis Exchange.
    Make a Donation and get one or all three discs today!

    STEX Collections

    By way of a "Thank You" gift, we'd like to send you our STEX Collector's DVD. It's some of the best buildings, lots, maps and mods collected for you over the years. Check out the STEX Collections for more info.

    Each donation helps keep Simtropolis online, open and free!

    Thank you for reading and enjoy the site!

    More About STEX Collections