Modifiers
Carbon modifiers are JSON plugin developer directed modifiers designed to enhance plugin designing experience by adding new field members in Rust classes and more.
A restart is necessary for the modifiers to apply (of course).
DIRECTORY
All Carbon modifier files go inside the carbon/modifiers
folder by default, it being changeable with -carbon.modifierdir
command-line.
RELEVANT SOURCE
Here is all of the relevant source code of things referenced on this page:
- Modifier is responsible for the IL generation and assembly post-processing events before the server boots.
- StoredModifiers is responsible for the logic of handling Rust entity
Save
andLoad
events, as well asProtoBuf.Net
subtype initialization.
Example
As stated previously, Carbon modifiers are in the format of JSON files, designed to be easily managed and shipped with plugins.
[
{
"Assembly": "Assembly-CSharp",
"Name": "BasePlayer",
"Fields": [
{
"Name": "IsSkibidi",
"Type": "System.Boolean",
"DefaultValue": true
},
{
"Name": "DuckCounter",
"Type": "System.Int32",
"DefaultValue": 0,
"IsStatic": true
},
{
"Name": "Tags",
"Type": "System.Collections.Generic.List`1[System.String]"
}
]
}
]
Then you can use it in your plugin(s) like this.
namespace Carbon.Plugins;
[Info("MyPlugin", "dev101", "1.0")]
public class MyPlugin : CarbonPlugin
{
private void OnServerInitialized()
{
foreach(var player in BasePlayer.allPlayerList)
{
// This compilation symbol is injected into the plugin when modifiers with the
// same file name as the plugin has been patched at boot time
#if MODIFIERS_PATCHED
player.Tags ??= new();
Puts($"{player} {(player.IsSkibidi ? "is" : "isn't")} skibidi.");
BasePlayer.DuckCounter++;
#endif
}
// Compilation fails if the modifiers haven't been patched
Puts($"Counted {BasePlayer.DuckCounter:n0} ducks.");
}
}
Stored Modifiers
IMPORTANT
This feature is designed to only work with Rust's entity system. So all stored modifiers must be inside in any classes that derive from BaseNetworkable
or BaseEntity
.
Saving & Loading
Carbon's modifier data save file is under <root>/server/<identity>/*.carbon.sav
. Carbon modifier data gets saved right before Rust's native server save saves and loads right after Rust's entities spawn.
You'll see something like this:
Spawning 0 entities from map
done.
Spawning 9932 entities from save
Processed 482 entities with Carbon modifier data
done.
Postprocessing 0 entities from map
done.
Postprocessing 0 entities from save
Starting to load entities into GlobalNetworkHandler...
Took 4ms to load entities into GlobalNetworkHandler
done.
Enforcing SpawnPopulation Limits
done.
Initializing 9985 entity links
done.
Initializing 87 stability supports
done.
Initializing 9934 entity save caches
done.
Upon server save, you'll see something like this (the log only happens when there actually is Carbon modifier stored data):
Invalidate Network Cache took 0.00 seconds
Saved 2,426 ents with Carbon modifier data
Saved 10,077 ents, cache(0.09), write(0.00), disk(0.00).
Saving complete
This is an extension to the previously mentioned modifiers, although access design slightly changes, as you'll notice shortly.
// carbon/modifiers/MyPlugin.json
{
"Assembly": "Assembly-CSharp",
"Name": "BasePlayer",
"Fields": [
{
"Name": "hasSeenWelcomeUi",
"Type": "System.Boolean",
"ShouldSave": true
},
{
"Name": "timeSinceUiOpen",
"Type": "System.Single",
"ShouldSave": true
}
]
}
MORE IMPORTANT STUFF
Carbon stored data only exists until the entity with that reference dies/gets killed. So once entities get killed, Carbon data is also killed.
How it works
On server boot, upon detecting and collecting all types that have stored modifiers, Carbon will generate the following new class in the same assembly, for each modified type.
// dnSpy, Assembly-CSharp, BasePlayerCarbonData
using System;
using Carbon.Components;
using ProtoBuf;
using ProtoBuf.Meta;
[ProtoContract(ImplicitFields = 1)]
public class BasePlayerCarbonData : StoredModifiers.Data
{
public static void Initialize()
{
RuntimeTypeModel.Default[typeof(StoredModifiers.Data)].AddSubType(151162043, typeof(global::BasePlayerCarbonData));
}
public bool hasSeenWelcomeUi = default(bool);
public float timeSinceUiOpen = default(float);
// This exists in a different carbon/modifiers JSON file,
// with a stored modifier on the same type
public int chatMessageCount = 0;
}
Then these following things happen in the modified type:
// dnSpy, Assembly-CSharp, BasePlayer
[NonSerialized]
// To actually save this data, you must assign this field yourself with a
// new value, then upon manual or automated Rust server save, it'll get
// picked up for saving. Setting it to null, will remove it from the save.
public global::BasePlayerCarbonData basePlayerCarbonData;
public override void Load(global::BaseNetworkable.LoadInfo info)
{
base.Load(info);
StoredModifiers.TryGetData<global::BasePlayerCarbonData>(this, ref this.basePlayerCarbonData, info);
// ...
}
public override void Save(global::BaseNetworkable.SaveInfo info)
{
base.Save(info);
StoredModifiers.TryUpdateData<global::BasePlayerCarbonData>(this, this.basePlayerCarbonData, info);
// ...
}
IMPORTANT
For you to access these new members inside your IDE, make sure to follow these instructions to get started.
You can then do the following in your plugin:
private void OnServerInitialized()
{
var player = BasePlayer.FindAwakeOrSleeping("Raul");
if (player == null)
{
return;
}
// Assigning the value of the data will tell Carbon that it should store it
player.basePlayerCarbonData ??= new BasePlayerCarbonData();
player.basePlayerCarbonData.timeSinceUiOpen = UnityEngine.Time.realtimeSinceStartup;
player.basePlayerCarbonData.hasSeenWelcomeUi = true;
// Setting it to null, will remove it from the Carbon modifier data save file
player.basePlayerCarbonData = null;
}