├── CHANGELOG ├── .gitignore ├── icon.png ├── Makefile ├── thunderstore.toml ├── Systems └── MyCoolModSystem.cs ├── Plugin.cs ├── Patches └── MyCoolModPatches.cs ├── .github └── workflows │ └── ci.yml ├── scripts └── rename.csx ├── LICENSE ├── README.md └── MyCoolMod.csproj /CHANGELOG: -------------------------------------------------------------------------------- 1 | # 0.1.0 2 | 3 | Initial Release -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | bin/ 3 | obj/ 4 | ts-build/ -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Captain-Of-Coit/cities-skylines-2-mod-template/HEAD/icon.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | BEPINEX_VERSION = 6 3 | 4 | clean: 5 | @dotnet clean 6 | 7 | restore: 8 | @dotnet restore 9 | 10 | build: clean restore 11 | @dotnet build /p:BepInExVersion=$(BEPINEX_VERSION) 12 | 13 | package-win: 14 | @-mkdir dist 15 | @cmd /c copy /y "bin\Debug\netstandard2.1\0Harmony.dll" "dist\" 16 | @cmd /c copy /y "bin\Debug\netstandard2.1\MyCoolMod.dll" "dist\" 17 | @echo Packaged to dist/ 18 | 19 | package-unix: build 20 | @-mkdir dist 21 | @cp bin/Debug/netstandard2.1/0Harmony.dll dist 22 | @cp bin/Debug/netstandard2.1/MyCoolMod.dll dist 23 | @echo Packaged to dist/ -------------------------------------------------------------------------------- /thunderstore.toml: -------------------------------------------------------------------------------- 1 | [config] 2 | schemaVersion = "0.0.1" 3 | 4 | [package] 5 | namespace = "CaptainOfCoit" 6 | name = "MyCoolMod" 7 | versionNumber = "0.1.0" 8 | description = "Example mod description" 9 | websiteUrl = "https://thunderstore.io" 10 | containsNsfwContent = false 11 | 12 | [package.dependencies] 13 | # BepInEx-BepInExPack = "5.4.2100" 14 | # CaptainOfCoit-HookUI = "0.3.0" 15 | 16 | [build] 17 | icon = "./icon.png" 18 | readme = "./README.md" 19 | outdir = "./ts-build" 20 | 21 | [publish] 22 | repository = "https://thunderstore.io" 23 | communities = [ "cities-skylines-ii", ] 24 | categories = [ "mods" ] 25 | -------------------------------------------------------------------------------- /Systems/MyCoolModSystem.cs: -------------------------------------------------------------------------------- 1 | using Game; 2 | using Game.Audio; 3 | using Game.Prefabs; 4 | using Game.Simulation; 5 | using Unity.Entities; 6 | using UnityEngine; 7 | using UnityEngine.InputSystem; 8 | using UnityEngine.Scripting; 9 | 10 | namespace MyCoolMod.Systems 11 | { 12 | public class MyCoolModSystem : GameSystemBase 13 | { 14 | // private SimulationSystem simulation; 15 | 16 | protected override void OnCreate() 17 | { 18 | base.OnCreate(); 19 | CreateKeyBinding(); 20 | // Example on how to get a existing ECS System from the ECS World 21 | // this.simulation = World.GetExistingSystemManaged(); 22 | } 23 | 24 | private void CreateKeyBinding() 25 | { 26 | var inputAction = new InputAction("MyModHotkeyPress"); 27 | inputAction.AddBinding("/n"); 28 | inputAction.performed += OnHotkeyPress; 29 | inputAction.Enable(); 30 | } 31 | 32 | private void OnHotkeyPress(InputAction.CallbackContext obj) 33 | { 34 | UnityEngine.Debug.Log("You pressed the hotkey, very cool! Good job matey"); 35 | } 36 | 37 | protected override void OnUpdate() {} 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Plugin.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Compression; 2 | using System.IO; 3 | using System; 4 | using System.Linq; 5 | using System.Reflection; 6 | using BepInEx; 7 | using HarmonyLib; 8 | using System.Collections.Generic; 9 | using System.Text; 10 | using UnityEngine; 11 | 12 | #if BEPINEX_V6 13 | using BepInEx.Unity.Mono; 14 | #endif 15 | 16 | namespace MyCoolMod 17 | { 18 | [BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)] 19 | public class Plugin : BaseUnityPlugin 20 | { 21 | private void Awake() 22 | { 23 | Logger.LogInfo($"Plugin {MyPluginInfo.PLUGIN_GUID} is loaded!"); 24 | 25 | var harmony = Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), MyPluginInfo.PLUGIN_GUID + "_Cities2Harmony"); 26 | var patchedMethods = harmony.GetPatchedMethods().ToArray(); 27 | 28 | Logger.LogInfo($"Plugin {MyPluginInfo.PLUGIN_GUID} made patches! Patched methods: " + patchedMethods.Length); 29 | 30 | foreach (var patchedMethod in patchedMethods) { 31 | Logger.LogInfo($"Patched method: {patchedMethod.Module.Name}:{patchedMethod.Name}"); 32 | } 33 | } 34 | 35 | // Keep in mind, Unity UI is immediate mode, so OnGUI is called multiple times per frame 36 | // https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnGUI.html 37 | private void OnGUI() { 38 | GUI.Label(new Rect(10, 10, 300, 20), $"Plugin {MyPluginInfo.PLUGIN_GUID} is loaded!"); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Patches/MyCoolModPatches.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Colossal.UI; 4 | using Game.SceneFlow; 5 | using Game.Audio; 6 | using Game.UI.Menu; 7 | using HarmonyLib; 8 | using System.Reflection.Emit; 9 | using Game; 10 | using MyCoolMod.Systems; 11 | 12 | namespace MyCoolMod.Patches { 13 | 14 | // This example patch adds the loading of a custom ECS System after the AudioManager has 15 | // its "OnGameLoadingComplete" method called. We're just using it as a entrypoint, and 16 | // it won't affect anything related to audio. 17 | [HarmonyPatch(typeof(AudioManager), "OnGameLoadingComplete")] 18 | class AudioManager_OnGameLoadingComplete 19 | { 20 | static void Postfix(AudioManager __instance, Colossal.Serialization.Entities.Purpose purpose, GameMode mode) 21 | { 22 | if (!mode.IsGameOrEditor()) 23 | return; 24 | 25 | // Here we add our custom ECS System to the game's ECS World, so it's "online" at runtime 26 | __instance.World.GetOrCreateSystem(); 27 | } 28 | } 29 | 30 | // This example patch enables the editor in the main menu 31 | [HarmonyPatch(typeof(MenuUISystem), "IsEditorEnabled")] 32 | class MenuUISystem_IsEditorEnabledPatch 33 | { 34 | static bool Prefix(ref bool __result) 35 | { 36 | __result = true; 37 | 38 | return false; // Ignore original function 39 | } 40 | } 41 | // Thanks to @89pleasure for the MenuUISystem_IsEditorEnabledPatch snippet above 42 | // https://github.com/89pleasure/cities2-mod-collection/blob/71385c000779c23b85e5cc023fd36022a06e9916/EditorEnabled/Patches/MenuUISystemPatches.cs 43 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: mycoolmod-ci 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | tags: [ "v*" ] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | bepinex-version: [5, 6] 14 | steps: 15 | - name: Checkout source 16 | uses: actions/checkout@v3 17 | - name: Checkout libcs2 18 | uses: actions/checkout@v3 19 | with: 20 | repository: Captain-Of-Coit/libcs2 21 | token: ${{ secrets.GH_PAT }} 22 | path: libcs2/ 23 | - name: Install .NET Core 24 | uses: actions/setup-dotnet@v3 25 | with: 26 | dotnet-version: 6.0.x 27 | - name: Build (BepInEx ${{ matrix.bepinex-version }}) 28 | run: make package-unix BEPINEX_VERSION=${{ matrix.bepinex-version }} 29 | - name: Upload Artifact (BepInEx ${{ matrix.bepinex-version }}) 30 | uses: actions/upload-artifact@v3 31 | with: 32 | name: built-code-${{ matrix.bepinex-version }} 33 | path: dist/*.dll 34 | publish: 35 | needs: build 36 | if: startsWith(github.ref, 'refs/tags/v') 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout source 40 | uses: actions/checkout@v3 41 | - name: Download built artifact 42 | uses: actions/download-artifact@v3 43 | with: 44 | name: built-code-5 45 | path: dist/ 46 | - name: Debug 47 | run: ls && ls dist/ 48 | - name: Download tcli 49 | run: | 50 | curl -L https://github.com/thunderstore-io/thunderstore-cli/releases/download/0.2.1/tcli-0.2.1-linux-x64.tar.gz -o tcli.tar.gz 51 | tar -xzf tcli.tar.gz 52 | - name: Publish with tcli 53 | run: ./tcli-0.2.1-linux-x64/tcli publish --token=${{ secrets.TS_TOKEN }} -------------------------------------------------------------------------------- /scripts/rename.csx: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using System.Linq; 5 | 6 | string[] args = Environment.GetCommandLineArgs(); 7 | 8 | if (args.Length < 3) { 9 | Console.WriteLine("Usage: rename.csx "); 10 | return; 11 | } 12 | 13 | var from = args[2]; 14 | var to = args[3]; 15 | Console.WriteLine($"Parameters - From: {from}, To: {to}"); 16 | 17 | string[] directoriesToIgnore = { 18 | ".git", 19 | "dist", 20 | "obj", 21 | "ts-build", 22 | }; 23 | 24 | var files = Directory.GetFiles(Environment.CurrentDirectory, "*.*", SearchOption.AllDirectories); 25 | Console.WriteLine("Starting file processing..."); 26 | 27 | foreach (var file in files) { 28 | Console.WriteLine($"Processing file: {file}"); 29 | 30 | // Check if the file's directory is in the ignore list 31 | string directory = Path.GetDirectoryName(file); 32 | if (directoriesToIgnore.Any(dir => file.StartsWith(Path.GetFullPath(dir)))) { 33 | Console.WriteLine($"Skipped file in ignored directory: {file}"); 34 | continue; 35 | } 36 | 37 | // Replace text inside files 38 | var content = File.ReadAllText(file, Encoding.UTF8); 39 | if (content.Contains(from)) { 40 | content = content.Replace(from, to); 41 | File.WriteAllText(file, content, Encoding.UTF8); 42 | Console.WriteLine($"Replaced content in {file}"); 43 | } 44 | 45 | // Rename files 46 | var fileName = Path.GetFileName(file); 47 | if (fileName.Contains(from)) { 48 | var newFileName = fileName.Replace(from, to); 49 | var newFilePath = Path.Combine(directory, newFileName); 50 | File.Move(file, newFilePath); 51 | Console.WriteLine($"Renamed file from {fileName} to {newFileName}"); 52 | } 53 | } 54 | 55 | Console.WriteLine("Processing completed."); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MyCoolMod - Copyright 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | Cities: Skylines 2 Mod Template - Copyright 2023 Captain-Of-Coit 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cities: Skylines 2 - C# Mod template 2 | 3 | This repository template allows you to get started with Cities: Skylines 2 modding easily, all the way to building your mod on commit with GitHub Actions and publishing your mod automatically on Thunderstore. 4 | 5 | - [Requirements](#requirements) 6 | - [Usage](#usage) 7 | - [Renaming your project](#renaming-your-project) 8 | - [Set license details](#set-license-details) 9 | - [Incrementing version number](#incrementing-version-number) 10 | - [CI / GitHub Actions - Setup](#ci-github-actions-setup) 11 | - [Regarding BepInEx version 5 (Stable) VS 6 (Alpha/Unstable/Nightly)](#regarding-bepinex-version-5-stable-vs-6-alphaunstablenightly) 12 | - [Credits](#credits) 13 | - [Community](#community) 14 | 15 | # Requirements 16 | 17 | - [Cities: Skylines 2](https://store.steampowered.com/app/949230/Cities_Skylines_II/) (duh) 18 | - [BepInEx 5.4.22](https://github.com/BepInEx/BepInEx/releases) or later 19 | - (Optional) [dotnet-script](https://github.com/dotnet-script/dotnet-script) (for `rename.csx` helper script) 20 | - Installation `dotnet tool install -g dotnet-script` 21 | 22 | # Usage 23 | 24 | - Create a new repository based on this one 25 | - Clone your new repository to your computer 26 | - Uncomment and update the `Cities2_Location` variable in `MyCoolMod.csproj` 27 | - Run `make build` 28 | 29 | After running the last command, the mod should be automatically copied to your game directory, 30 | so launching the game should include running the mod you just started :) 31 | 32 | # Renaming your project 33 | 34 | You can leverage the helper script in `scripts/rename.csx` in order to replace "MyCoolMod" with whatever you want to name your project. Usage: 35 | 36 | ``` 37 | $ dotnet script scripts\rename.csx "MyCoolMod" "AnotherModIMade" 38 | ``` 39 | 40 | # Set license details 41 | 42 | You'll need to update `LICENSE` with the correct details for `` and ``, and change "MyCoolMod" to your mod name if you haven't already. 43 | 44 | # Incrementing version number 45 | 46 | - Update `.csproj` file with new version number 47 | - Update `thunderstore.toml` file with new version number 48 | - Update `CHANGELOG` to describe the changes you've made between this and previous version 49 | - Commit version bump 50 | - Do a git tag with the new version number 51 | - `git tag -a v0.2.0 -m v0.2.0` 52 | - Push your changes + tags 53 | - `git push origin master --tags` 54 | 55 | # CI / GitHub Actions - Setup 56 | 57 | In order to get the CI/GitHub Actions workflow to work, you have to do a couple of things. 58 | 59 | - Create a new private repository with all the game DLLs that you require for building your mod 60 | - Create a new GitHub Personal Access Token ("PAT") that has only READ access to the created private repository 61 | - Create a new secret variable in GitHub Actions called `GH_PAT` that has your PAT with read access to the private repository 62 | 63 | Now the CI job should work as expected :) 64 | 65 | # Regarding BepInEx version 5 (Stable) VS 6 (Alpha/Unstable/Nightly) 66 | 67 | Currently, this mod template defaults to building against BepInEx version 6 (unstable pre-release). If you'd like to instead use Stable BepInEx version 5, you can run the build like this: 68 | 69 | ``` 70 | $ make build BEPINEX_VERSION=5 71 | ``` 72 | 73 | In order to run code only for one BepInEx version, you can do something like this: 74 | 75 | ``` 76 | #if BEPINEX_V6 77 | using BepInEx.Unity.Mono; 78 | #endif 79 | ``` 80 | 81 | That would only run `using BepInEx.Unity.Mono` when you're building the project for BepInEx 6. Add in a `else` if you want to do something different when it's version 5. 82 | 83 | # Credits 84 | 85 | - Thanks to Cities Skylines 2 Unofficial Modding Discord 86 | - Particular thanks to [@StudioLE](https://github.com/StudioLE) who helped with feedback and improving .csproj setup 87 | 88 | # Community 89 | 90 | Looking to discuss Cities: Skylines 2 Unofficial modding together with other modders? You're welcome to join our "Cities 2 Modding" Discord, which you can find here: https://discord.gg/vd7HXnpPJf 91 | -------------------------------------------------------------------------------- /MyCoolMod.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | netstandard2.1 12 | MyCoolMod 13 | MyCoolMod 14 | A mod that I made for Cities: Skylines 2 15 | 0.1.0 16 | true 17 | latest 18 | 19 | https://api.nuget.org/v3/index.json; 20 | https://nuget.bepinex.dev/v3/index.json; 21 | https://nuget.samboy.dev/v3/index.json 22 | 23 | 24 | true 25 | 26 | MSB3277 27 | 28 | 29 | 33 | 34 | C:\Program Files (x86)\Steam\steamapps\common\Cities Skylines II 35 | 36 | 37 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 53 | 54 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 5 77 | 78 | 79 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | $(DefineConstants);BEPINEX_V6 94 | 95 | 96 | 97 | 99 | 100 | 101 | 105 | 106 | 108 | 109 | --------------------------------------------------------------------------------