Code Modding
You can compile C# files or load dll libraries directly in Software Inc.
A good grasp of C# programming and the Unity3D API is required to make a code based mod.
Setup
Project setup
To create your own mod start a .NET Class Library project in Visual Studio, targeted at the .NET 4 profile. You should download and install the .NET 4 Framework if you don't have it, .NET Core is not the same.
You should add a reference to the following libraries: Note that you might need to add references to more, depending on what you want to do.
- Software Inc_Data\Managed\UnityEngine.UI.dll
- Software Inc_Data\Managed\UnityEngine.dll
- Software Inc_Data\Managed\UnityEngine.CoreModule.dll
- Software Inc_Data\Managed\Assembly-CSharp.dll
- Software Inc_Data\Managed\Assembly-CSharp-firstpass.dll
Start by creating a class that implements the ModMeta abstract class, then implement as many classes that inherit from ModBehaviour as you want.
The ModMeta class will contain information about your mod and act as a manager for your mod.
namespace myMod
{
internal class MyModMeta : ModMeta
{
public override void ConstructOptionsScreen(RectTransform parent, bool inGame)
{
Text text = WindowManager.SpawnLabel();
text.text = "This is the description of my mod\nIt shows in dropdown 'My Mod!' in the 'Mods' section of the options area.";
WindowManager.AddElementToElement(text.gameObject, parent.gameObject, new Rect(0f, 0f, 400f, 128f),
new Rect(0f, 0f, 0f, 0f));
}
public override string Name => "My Mod!";
}
}
ModBehaviours are just a subclass of Unity's MonoBehavior and work the same way, i.e. they use the same life cycle of Awake, Start, Update, OnDestroy, ModBehaviours only differ in that they have the abstract OnActive and OnDeactivate methods, to signal when the mod is toggled. Each ModBehaviour implementation will be instantiated once the mod is loaded.
Compile
When you're done, create a subfolder for your mod in a folder called "DLLMods" in the game's root directory and put your compiled .dll file in it, or .cs files if you want the game to compile them for you. Note that if you let the game compile the C# files for you, you are limited to C# version 3, but using the game's compiler is required if you want to upload your mod to the Steam Workshop.
If you use the game's compiler, note that it will inject error handling code into each function body, you can disable this behaviour while you are developing your mod using the launch paramater -DisableModErrors.
Please note that due to a bug in the compiler the game uses, you cannot use enums if you are using straight .cs files or the game will crash
Example mod
Here's a working mod example that lets you change how many floors you can build on, complete with comments: Floor Mod (Updated to work with Alpha 10.7+)
Dependencies
If you want to reference external dll files, you can put them in the Software Inc_Data\Managed folder.
You can also add this code snippet to your ModMeta class, to load dll files from a subfolder: (Note that this requires GiveMeFreedom and it might not find the correct dll filename, so you might need to adjust it to your situation)
public override void Initialize(ModController.DLLMod parentMod)
{
AppDomain.CurrentDomain.AssemblyResolve += (x, y) => Assembly.LoadFrom(Path.Combine(parentMod.FolderPath(), "'''SubFolder'''/" + y.Name.Substring(0, y.Name.IndexOf(",")) + ".dll"));
base.Initialize(parentMod);
}
Compatibility
From Beta 1.7+ you can use define symbols for non-dll-based mods that are compiled by the game, e.g.
#if !SWINCBETA && !SWINCRELEASE
//(this is beta pre 1.7)
#elif SWINCBETA1_7 || SWINCBETA1_8
//(Something broke in beta 1.9, so this is sectioned off)
#elif SWINCBETA1_9 || SWINCBETA1_10
//(Something broke in beta 1.11, so this is sectioned off)
#else
//(This is for all future versions beta 1.11+)
#endif
The game will define 3 symbols SWINCTYPE (SWINCBETA or SWINCRELEASE), SWINCTYPEMAJOR (SWINCBETA1, SWINCBETA2, etc.) and SWINCTYPEMAJOR_MINOR (SWINCBETA1_7, SWINCBETA1_8, etc.)
Getting Started
Debugging console
Before you do anything, you should enable the in-game debugging console by binding the console key at the bottom of the key binding menu in the options window. This will help you debug your mod by giving you error messages and enabling the RECOMPILE_DLL_MOD, RELOAD_DLL_MOD and UNLOAD_DLL_MOD commands.
Helpful commands
The EXECUTE command will allow you to execute arbitrary SIPL code (which closely resembles C#) while the game is running. E.g. if you wanted to find the highest paid employee you could write EXECUTE GameSettings.sActorManager.Actors.OrderByDescending(x.employee.Salary).First() or if you wanted to color all selected rooms' exterior green you could write EXECUTE Selected.Where(x is Room).ForEach(x.OutsideColor = Color(0,1,0)).
Here are some handy variables and functions you can use with EXECUTE:
| GameSettings | References the currently active instance of GameSettings |
| SelectorController | References the currently active instance of SelectorController |
| MarketSimulation | References the currently active instance of MarketSimulation |
| Selected | List of objects currently selected in-game |
| LastProduct | Reference to last product opened in a detail window |
| LastCompany | Reference to last company opened in a detail window |
| WorkItems | List of the active player company's tasks (GameSettings.MyCompany) |
| Now | Current in-game date |
| CopyToClipboard(o) | Copies o to the clipboard, wrap your entire statement in this to put the result in the clipboard |
| DoSelect(o) | Selects o in-game, can be an object or a list, e.g. DoSelect(GameSettings.sActorManager.Actors) |
The SHOW_INSPECTOR command opens a window that allows you to inspect all objects in the active scene.
Note on threading
You should stick to the ModBehaviour's Update method, rather than trying to write your own update loop with threads or timers, as you cannot interact with Unity's system from other threads and you will most likely end up causing race conditions when interacting with the game's systems. You can use Unity's Time class to keep track of elapsed time between frames.
Saving and loading data
Global data
All ModBehaviours have SaveSetting and LoadSetting methods to save data globally.
Note that SaveSetting and LoadSetting are generic and will call ToString() and try to convert the saved string back to the data type you specify when loading, so this will only work for simple types, like integers, floats, doubles, bools, strings, etc.
To load something back, use LoadSetting<type>("Key", defaultvalue), e.g. LoadSetting<bool>("Notifications", false) or LoadSetting("Notifications", false), since the type bool is implicit from using false.
There's also TryLoadSetting, if you want to handle when the setting doesn't exist and DeleteSetting to remove existing settings.
Per save data
You can also save data to the player's save file, to keep settings per save, by overriding your ModBehaviours' Serialize and Deserialize methods. These use a WriteDictionary, which is just a dictionary of whatever you want, and the game will handle turning it into bits.
The WriteDictionary is powerful, in that it has no limitations on the data you can save, but please note that any custom classes saved using this method has to have an empty constructor. If you want some fields not to be serialized, use the [System.NonSerialized] attribute. Properties are not serialized, so you need to create backing fields. Saving custom classes from your mod will also break saves if the mod is no longer installed, so it's best to avoid when you can.
When you want to save data, override and populate the WriteDictionary with values in the Serialize(WriteDictionary data, GameReader.LoadMode mode) method, this will then be returned when a save is loaded in the Deserialize(WriteDictionary data, GameReader.LoadMode mode) method. If you wanted to save a setting you can write data["Notifications"] = true and load it back with Notifications = data.Get("Notifications", true)
There is also a LoadMode parameter, which tells you whether it was a full save, a build mode save or a company save (Usually used when the player is moving to another map or during multiplayer synchronization), which will allow you to only save data needed for each type. In most cases it's best to avoid saving anything for a build mode save.
You can also override the serialization methods in the ModMeta class to take complete control of saving data. Returning null will make the mod not save any data. To help you with serializing data from your ModBehaviours, the ModMeta class has a ModBehaviours list of all active ModBehaviours that were created when the mod was first loaded. However, the default behaviour is to call Serialize and Deserialize on your ModBehaviours in turn and check if they saved any data, so just leaving it as is will work in most cases.
Note that your the save data is identified by the Name you've set in the ModMeta class. If you change this name, it will no longer be able to find the correct data from older saves and name clashes will result in data loss.
Reading external files
All modbehaviours have access to their ParentMod, this is the class that manages your mod, and through this you can load files using the methods LoadTexture(png, jpg, jpeg), LoadXMLFile(xml, only first tag), LoadFullXMLFile(xml, all tags in a list), LoadTydFile(tyd), LoadAudio(mp3, wav, ogg), LoadGLTF(gltf, glb, only loads the first mesh and its morph targets) and LoadOBJ(obj). Note that these methods all use relative paths from where your mod is installed and using ../ won't work.
Creating UI
Button in the main HUD
You can add buttons to the bottom main HUD by calling HUD.Instance.AddBottomButton(string panel, string name, string desc, Sprite icon). You can use one of the built-in icons by calling ObjectDatabase.Instance.GetIcon(name) and use the console command SHOW_ICONS to see all the built-in icons, or if you want your own Icon you need to instantiate a Sprite with a texture that you load using ParentMod.LoadTexture or in the ModMeta's Initialize(ModController.DLLMod parentMod) method. The game uses 32x32 icons that are completely white with a transparent background, if you want to stay consistent.
HTML-like UI
You can load in UIs using xml files with an html like structure using WindowManager.GenerateUI(List<XMLParser.XMLNode> nodes, GameObject parent, ModBehaviour callback). Example:
<Window MinSize="0,0" NonLocTitle="Test window"> <ScrollView anchor="fill" position="4,4" size="-8,-8"> <VerticalLayout anchor="top" padding="4,4,4,4" spacing="2" childForceExpandHeight="False" childControlHeight="False"> <ContentFitter verticalFit="PreferredSize"> <Label id="lab" color="FF0000" height="24">Hello</Label> <Button onClick="ShowMessage(Hehe)" height="24">Click me</Button> <Combo OnSelectedChanged="ShowMessageCombo(this)" height="24">a,b,c</Combo> </ContentFitter> </VerticalLayout> </ScrollView> </Window>
This will create a window with a scroll view containing a label, a button and a combobox automatically laid out vertically. Please note that attributes, e.g. position="4,4" are case sensitive!
To position an element you can use the anchor attribute to decide where the element should be anchored to in the element it is sitting in. The anchor is basically a box that contains the element formatted as x,y,width,height, where all values should be between 0-1, representing 0-100% in the parent element. An anchor of (0,0,0,0) is just the top left corner of the element and (0,0,1,1) is an anchor that fills the entire element, since width and height is 1=100%. position is the x, y pixel offset from the top left of the anchor. size or width and height is the pixel offset from the bottom right corner of the anchor. anchor can be formatted as either "#,#,#,#", "middle,center", "bottom,right", "top"(to fill the entire top), "left"(to the fill the left side) or "fill" to fill the entire element, etc. Note that in the scroll view I'm using anchor="fill" position="4,4" size="-8,-8", because I want it to fill the parent window with a 4 pixel margin, so I have to decrease the size by 4 x 4 = 8, which is why size is -8,-8.
You can also use the id attribute to reference the element in your code after the UI has been loaded.
The game will try to find a corrosponding variable to set for the element for any other attributes you use (Remember case sensitivity). Some elements, like the button, input and combo have event variables, i.e. onClick, onValueChanged and OnSelectedChanged (use visual studio's code lookup feature to see what is possible), these can be set to function calls that will be called directly on an object you set when creating the UI, i.e. the onClick="ShowMessage(Hehe)" and OnSelectedChanged="ShowMessageCombo(this)" part. Note that using "this" will refer to the actual component the tag represents. Here's an example of how you would create and use this UI:
var elements = WindowManager.GenerateUI(ParentMod.LoadFullXMLFile("UI.xml"), null, this);
var label = (Text)elements["lab"];
Debug.log(label.text);
public void ShowMessage(string x)
{
WindowManager.Instance.ShowMessageBox(x, true, DialogWindow.DialogType.Information);
}
public void ShowMessageCombo(GUICombobox x)
{
WindowManager.Instance.ShowMessageBox(x.SelectedItemString, true, DialogWindow.DialogType.Information);
}
Note that the parent parameter of GenerateUI call is null, because all the UI will be added as a child to the parent object, but since we are spawning a window, this is not necessary to define. You can use WindowManager.Spawn* to spawn UI elements to put your UI into. I'm using this for the callback parameter, so when you click the button, the method ShowMessage from the current class will be called.
Note that creating UI like this is done on the fly, so you could add functionality in your mod to reload UI from an XML while the mod is still running for rapid testing.
These are the current object tags that will allow you to create a new UI element:
- empty
- panel
- button
- label
- list
- input (input field for text)
- checkbox
- progressbar
- slider
- combo
- scrollbar
- scrollview
- window
- image
- rawimage
These are the current layout tags that will allow you to add a component to the UI element it is defined under. Any position and anchor changes will be applied to the parent:
- horizontallayout (see https://docs.unity3d.com/2018.2/Documentation/ScriptReference/UI.HorizontalLayoutGroup.html)
- verticallayout (see https://docs.unity3d.com/2018.2/Documentation/ScriptReference/UI.VerticalLayoutGroup.html)
- gridlayout (see https://docs.unity3d.com/2018.2/Documentation/ScriptReference/UI.GridLayoutGroup.html)
- contentfitter (see https://docs.unity3d.com/2018.2/Documentation/ScriptReference/UI.ContentSizeFitter.html)
- layoutelement (see https://docs.unity3d.com/2018.2/Documentation/ScriptReference/UI.LayoutElement.html)
Note that when adding tags to a window or scrollview, you are not adding it directly to the window or scrollview, but their content panels. For the rest of the elements, children tags are added directly to the element.
Networking
To send data to players in multiplayer you first need to call ParentMod.RegisterNetworkID(id) in a ModBehaviour. The id should be a value between 1-255, it will be used to identify messages to and from your mod, so you should pick a unique number that is unlikely to clash with other mods. When your mod is deactivated, the id will automatically be de-registered and will have to be re-registered to work. Please note that if players don't enable code mods when they host a game, all code mods will be deactivated immediately.
To send messages you have to call ParentMod.SendNetworkMessage(byte[] data, ModBehaviour.MessageTarget target, byte targetID) in your ModBehaviour, where target can be everyone(default), everyone but me(so avoid sending message to self), everyone except(everyone except player with targetID), specifically (only player with targetID) and host. For performance reasons, I recommend instantiating a MemoryStream when your mod first loads and when you want to send data call MemoryStream.SetLength(0) to reset its data and then use the extension methods that Software Inc. comes with to write arbitrary data to the stream. Finally call MemoryStream.ToArray() to send the data. Please be careful about how much data you send, you should keep the packages as small as possible.
When you want to receive data, override ModMeta.ReceiveNetworkMessage(SINetworking.NetworkPlayer player, MemoryStream stream), and just use the Stream extension methods to turn the bytes in the MemoryStream back into data you use, in the same order you sent it.
You should start by deciding how you want to layout your data. A good idea is to use the first byte to identify what type of message you are sending. You should also send a ping message to everyone in the start of a game, to check if other players also have the mod installed. You can use ParentMod.GetCurrentPlayers() to see who is currently connected (this includes the local player).
The extension methods Software Inc. has for Stream objects include the ability to write any type you want, with simple types like ints, bools and strings having the best performance. They will be called ReadX or WriteX. It also has generic methods to write lists and dictionaries. If you implement the interface IByteData and add a static Type ReadData(Stream st) method for a class, you can also quickly write that class using the Stream.Read/WriteByteObject extension methods. You can also use Stream.WriteObject to write arbitrary data, however this will quickly balloon in size and is very slow, so use it carefully,
Full access
By default, certain namespaces and types are off-limits to mods for security reasons. If you want to make a mod that writes to files or accesses the internet, you need to put a public static bool called GiveMeFreedom in your ModMeta implementation. Note that this only works for dll-based mods, which can't be uploaded to the Steam Workshop, and the user will be warned.
Events
As of Alpha 11.6.5.
Note that no MarketSimulation events are raised during the initial market simulation to avoid race conditions.
| GameSettings.IsDoneLoadingGame | Raised when a new game has finished loading. Beware that this can be raised before the initial market simulation is done. |
| GameSettings.GameReady | Raised when the game is guaranteed to be finished loading and the player has full control. |
| GameSettings.OnQuit | When the player quits an active game. |
| GameSettings.Instance.OnServersChanged | When servers have been added or removed. Usually used to keep server dropdowns updated |
| MarketSimulation.OnProductReleased + OnAddOnReleased | When a product is released, but does not count Mock products, which are products that are only available to support during beta for the player. |
| MarketSimulation.OnProductRemoved + OnAddOnRemoved | When a product is removed. Products are usually removed from the game after 20 years of inactivity to keep the save file from bloating. |
| MarketSimulation.OnCompanyFounded | When a new company is founded, not including the player's company. |
| MarketSimulation.OnCompanyClosed | When a new company is closed down. |
| MarketSimulation.OnTechResearched | When new a new tech level is researched, not including the initial tech levels. |
| MarketSimulation.OnFrameworkReleased | When a framework is released. |
| TimeOfDay.OnHourPassed | When an hour has passed, after everything has updated (server refresh, etc.). |
| TimeOfDay.OnDayPassed | When a day has passed, after everything has updated (market simulation, etc.). This is called right before OnMonthPassed when a month passes, so it is basically the same as OnMonthPassed with 1 days per month, except it happens before bills are processed. |
| TimeOfDay.OnMonthPassed | When a month has passed, after everything has updated (bills, etc.). |
Entry points
| GameSettings.Instance | This class contains most of the objects that manage the game |
| GameSettings.Instance.MyCompany | The player's company |
| MarketSimulation.Active | Manages all companies and products |
| GameSettings.Instance.sRoomManager | Manages rooms, furniture and room segments |
| GameSettings.Instance.sActorManager | Manages employees and teams |
| SelectorController.Instance | Manages selection |
| TimeOfDay.Instance | Manages time |
| HUD.Instance | Manages the main HUD and windows |
| ObjectDatabase.Instance | Contains all furniture and room segments |
| WindowManager | Controls windows and has functions to create windows and GUI elements |