Skip to the content.

This is a step-by-step guide for creating Buff Debuff Demo, a mod that uses the Buff & Debuff System to give beavers:

Random Buff

TOC

  1. Refer to the mod assembly (DLL)
  2. Create Positive Buff
    1. Create PositiveBuff class
    2. Create PositiveBuffInstance class
    3. Create SpeedBuffEffect class
    4. Register the Buff
    5. Test the Buff
  3. Create BeaverBuffComponent
    1. Create BeaverBuffComponent class
    2. Register the Component
    3. Test the Buff
  4. Create Lucky Buff (with Timed Buff Instance)
    1. Create LuckyBuff class
    2. Create LuckyBuffInstance class
    3. Register the Buff
    4. Test the Buff

1. Refer to the mod assembly (DLL)

manifest.json

{
    // Other entries
    "RequiredMods": [
        {
            "Id": "BuffDebuff",
            "MinimumVersion": "7.0.0"
        }
    ]
}

BuffDebuff.csproj

<PropertyGroup>
    <BuffDebuffPath>$(ModPath)3433810580\version-0.7\BuffDebuff.dll</BuffDebuffPath>
</PropertyGroup>

<ItemGroup>
    <Using Include="BuffDebuff" />

    <Reference Include="$(BuffDebuffPath)">
        <Private>False</Private>
    </Reference>		
</ItemGroup>

[!NOTE]

Optional: make Localization entries

Add Localizations\enUS.csv file (if not exist) to your mod to support localization. You need a few entries like the name and description for the buff. For example:

ID,Text,Comment
LV.BuffDebuffDemo.PositiveBuff,"Positive of the day",The name of the positive buff
LV.BuffDebuffDemo.PositiveBuffDesc,"Your beavers are feeling good today",The description of the positive buff
<More content...>

2. Create Positive Buff

First we create a positive buff that gives beavers a random movement speed buff (0% - 100%) every day.

Each buff consists of a Buff class and one or more BuffInstance classes. The Buff class is a singleton that manages the buff instances. It also provides general information about the buff, such as its name and description.

The BuffInstance class defines the targets and effects of the buff. It is the actual instance that is attached to the entities in the game.

2.1 Create PositiveBuff class

See full content with helpful comments here: PositiveBuff.cs

// This is a singleton IBuff instance. It is not applied to the entities (Buffable) directly but it provides some information about the buff.
// Think of it as a Manager for the buff. It is created once and then it creates the instances of the buff.
public class PositiveBuff(ISingletonLoader loader, IBuffService buffs, ILoc t, EventBus eb) : SimpleFloatBuff<PositiveBuff, PositiveBuffInstance>(loader, buffs), IUnloadableSingleton
{
    // ...
}
// The unique key to save and load the buff, in this case we only need to save the Id,
// The logic is already implemented in the base class
static readonly SingletonKey SaveKey = new("PositiveBuff");
protected override SingletonKey SingletonKey => SaveKey;
// The name and description of the buff
public override string Name { get; protected set; } = t.T("LV.BuffDebuffDemo.PositiveBuff");
public override string Description { get; protected set; } = t.T("LV.BuffDebuffDemo.PositiveBuffDesc");
    // Register and unregister this service for the event we need (DaytimeStartEvent)
    protected override void AfterLoad()
    {
        base.AfterLoad();
        eb.Register(this);
    }

    public void Unload()
    {
        eb.Unregister(this);
    }

    // We update the buff instances when the day start
    [OnEvent]
    public void OnDaytimeStart(DaytimeStartEvent _)
    {
        // ...
    }
// Remove the previous buff
// There should be only one but we remove all just in case
// This logic can actually be processed with the BuffInstance as well if you want to
// but in this case we keep it here so we don't have to store the day of the buff instance.
var existing = buffs.GetInstances<PositiveBuffInstance>();
foreach (var i in existing)
{
    buffs.Remove(i);
}
const float MaxBuffPerc = 1f; // 100%;

// Add the new buff
var perc = UnityEngine.Random.Range(0, MaxBuffPerc); // Get the random speed boost percentage
var instance = CreateInstance(perc); // CreateInstance is implemented in the base class
buffs.Apply(instance);

[!NOTE]
At the end of the sample file, I also added a commented out section on one of the way to process the buff using BuffAddedToEntityEvent and BuffRemovedFromEntityEvent though it’s not recommended. You can check it out if you are curious.

2.2 Create PositiveBuffInstance class

See full content with helpful comments here: PositiveBuffInstance.cs

public class PositiveBuffInstance : SimpleFloatBuffInstance<PositiveBuff>
{
    // ...
}
// Indicate this is a positive buff and will show up in the positive buff panel
public override bool IsBuff { get; protected set; } = true;

![TIP]
This means you can create a single IBuff that generate both Buff & Debuff. For example, instead of creating two separate buffs a day that cancel one another, I can create a single buff and set IBuff like this:
public override bool IsBuff { get; protected set; } = Value >= 0;

// These need to be set later
public override IEnumerable<IBuffTarget> Targets { get; protected set; } = [];
public override IEnumerable<IBuffEffect> Effects { get; protected set; } = [];

![Caution]
BuffInstance has to have a new() (parameterless) constructor. You then use InjectAttribute to inject necessary services into the instance similar to how you code a BaseComponent. That’s why at the time of inititalization, we don’t have the injected services, Value or Buff available yet.

// We cannot inject them with constructor because this class must have a parameterless constructor
// So we inject them with InjectAttribute instead
IBuffableService buffables = null!;
EventBus eb = null!;
ILoc t = null!;

// Add anything you need to inject here
[Inject]
public void Inject(IBuffableService buffables, EventBus eb, ILoc t)
{
    this.buffables = buffables;
    this.eb = eb;
    this.t = t;
}
// Here for this instance we know there is only one Effect of one kind so we give it directly
// so we can easily access it later without iterating through the Effects
// Note that we cannot init it here because Value is still not set when the class is created yet.
public SpeedBuffEffect Effect { get; private set; } = null!;

// This method is called after the BuffInstance was created and injected. Buff property and Value properties are set.
// Init is also called when the game is loaded and the values are loaded from the save.
public override void Init()
{
    // We set the targets: GlobalBeaverBuffTarget and similar are already implemented by the system
    Targets = [new GlobalBeaverBuffTarget(buffables, eb)];
    
    // Then we add the speed effect:
    Effect = new SpeedBuffEffect(t, Value);
    Effects = [Effect];
}
// In case you need to modify the save and load logic, you can override these methods
// The base class already saves and loads the Value property for you so in this case we do not need to modify it

// If you do not want to save an instance (for example, you can always create it by the IBuff on load), return null
// If you return null, the instance will not go into the game file
protected override string? Save()
{
    return base.Save();
}

// If you update your mod and no longer want to load a save, or the save data is not desired any more, return false
// If you return false, the instance created will then be discarded.
protected override bool Load(string savedState)
{
    return base.Load(savedState);
}

![TIP]

2.3 Create SpeedBuffEffect class

See full content with helpful comments here: SpeedBuffEffect.cs

// This is an effect a buff should have.
// Use it however you want. In this project, we can reuse this one for all 3 buffs.
// In your game, you may have different effects to show up in the tooltip.
public class SpeedBuffEffect(ILoc t, float speed) : IBuffEffect
{
    // ...
}
// Show as percentage
const string Format = "+#%;-#%;0%";

// This will show up in the tooltip, something like:  Movement speed: +50%
public string? Description { get; } = t.T("LV.BuffDebuffDemo.SpeedBuffEffectDesc", speed.ToString(Format));
// For the other code to access and process the speed bonus
public float Speed => speed;
public long Id { get; set; }

public void CleanUp() { }
public void Init() { }
public void UpdateEffect() { }

2.4 Register the Buff

In your mod configuration (ModConfigs.cs in this project), register the buff:

// Register these under the Game context
[Context("Game")]
public class GameConfig : Configurator
{
    public override void Configure()
    {
        // Buffs should be singleton
        Bind<PositiveBuff>().AsSingleton();

        // ...
    }
}

2.5 Test the Buff

Now you can test the buff in the game. Load the game with this mod and wait for the day to start to see the buff applied to the beavers.

![TIP]
Use Dev Mode (Alt + Shift + Z) to speed up the game time. You can press “Skip to next day time” some times to get to the next day (7 is the hotkey)

Positive Buff

You can also try saving the game, then reloading it to make sure the value is saved and loaded correctly. And try skipping to the next day to see the buff value changed.

However you can see two problems:

  1. The buff is not applied until the next day. We will fix this later.
  2. The buff is there but no actual effect is there because there is no logic to apply the speed boost.

3. Create BeaverBuffComponent

To apply the speed boost to the beavers, we attach a BaseComponent to all Beavers. This component will listen to the BuffAddedToEntityEvent and BuffRemovedFromEntityEvent events and apply the speed boost when the positive buff is added.

3.1 Create BeaverBuffComponent class

See full content with helpful comments here: BeaverBuffComponent.cs

// This is a BaseComponent that will attach to the Beaver entities only (due to our declaration in the Config)
// This is the recommended way to process the buff instances compared to the commented out code in PositiveBuff
// because only entities that can receive the buff will have this component
public class BeaverBuffComponent : BaseComponent
{
    // ...
}
// We need the BonusManager to apply the bonus to the entity
BonusManager bonusManager = null!;
// We need the BuffableComponent to unregister the event
BuffableComponent buffable = null!;

// When the object is initialized, we grab the BuffableComponent and register the event
public void Awake()
{
    // We know each Beaver has a BonusManager attached (from the game code)
    bonusManager = GetComponentFast<BonusManager>();

    buffable = GetComponentFast<BuffableComponent>();
    // Register the event
    buffable.OnBuffAdded += Buffable_OnBuffAdded;
    buffable.OnBuffRemoved += Buffable_OnBuffRemoved;
    buffable.OnBuffActiveChanged += Buffable_OnBuffActiveChanged;
}

public void OnDestroy()
{
    // Unregister the event
    buffable.OnBuffAdded -= Buffable_OnBuffAdded;
    buffable.OnBuffRemoved -= Buffable_OnBuffRemoved;
    buffable.OnBuffActiveChanged -= Buffable_OnBuffActiveChanged;
}

![NOTE]
According to the usual game code, maybe the event registration should be registered in Start() instead of Awake(). It’s up to you to decide.

// Here we can narrow down to our own Instance, or we can just process indiscriminately for all buffs
// as long as they have the SpeedBuffEffect
private void Buffable_OnBuffAdded(object sender, BuffInstance e)
{
    // We can process all the speed effect here
    if (!e.Active) { return; }

    var speedEffect = e.Effects.Where(q => q is SpeedBuffEffect).Cast<SpeedBuffEffect>();
    foreach (var effect in speedEffect)
    {
        bonusManager.AddBonus(Constants.MovementSpeedBonusId, effect.Speed);
    }
}
private void Buffable_OnBuffRemoved(object sender, BuffInstance e)
{
    // Process similarly but to remove the bonus
    if (!e.Active) { return; }

    var speedEffect = e.Effects.Where(q => q is SpeedBuffEffect).Cast<SpeedBuffEffect>();
    foreach (var effect in speedEffect)
    {
        bonusManager.RemoveBonus(Constants.MovementSpeedBonusId, effect.Speed);
    }
}
// We know that for our case this one does not happen (we do not Activate/Deactivate our buff instances)
// But if you do, you can process it here
private void Buffable_OnBuffActiveChanged(object sender, BuffInstance e)
{
    if (e.Active)
    {
        Buffable_OnBuffAdded(sender, e);
    }
    else
    {
        Buffable_OnBuffRemoved(sender, e);
    }
}

3.2 Register the Component

In your mod configuration (ModConfigs.cs), register the component:

public override void Configure()
{
    // ...

    // Decorator Component for the Beavers as the recommended way to process the Buffs
    MultiBind<TemplateModule>().ToProvider(() =>
    {
        TemplateModule.Builder b = new();
        b.AddDecorator<BeaverSpec, BeaverBuffComponent>();
        return b.Build();
    }).AsSingleton();
}

3.3 Test the Buff

Now load the game again, you should see the speed boost applied to the beavers immediately when the buff is added (see that stat below the buff panel).

Try going to the next day to see the buff value changed and the correct speed boost applied to the beavers.

Practice 1

Fix the Buff not applied until the next day

When the user first loads the game with your mod newly installed, or when they start a new game, the buff is not applied until the next day. You can try thinking of a way to fix it.

You can see the solution in the PositiveBuff.cs file. I folded the code in #region so you can hide it if you want to try it yourself.

[!CAUTION]
You should not just apply an extra buff if you see no BuffInstance is applied on Load or even PostLoad. BuffInstances are loaded on PostLoad and they relies on IBuff so they are not there yet.

Tip 1 You can add a property to determine if an initial buff is applied or not.
Tip 2 To save additional properties, you should override LoadSingleton and SaveSingleton instead of Load and Save to retain the logic of the base class. > ![IMPORTANT] > You should call `base.LoadSingleton` and `base.SaveSingleton` in your override.

Practice 2

Create a Negative Buff

Create a negative buff that gives beavers a random movement speed debuff (0% - 100%) every day. The code is very similar to the positive buff, you can reuse a lot of the code.

Negative Buff

You can see the solution in the NegativeBuff.cs and NegativeBuffInstance.cs files.

Tip 1 You can just reuse SpeedBuffEffect for this buff as well, just with a negative value. This way you don't even need to create any new processing logic.
Tip 2 If you don't see the buff, make sure you have the NegativeBuff registered in the mod configuration.

4. Create Lucky Buff (with Timed Buff Instance)

Each day, the beavers also get a random movement speed buff that lasts for a random amount of time between 0 to 30 hours. This is a bit more complex because we need a timed buff instance, and this time there is a chance two or more buff instances are present at the same time.

4.1 Create LuckyBuff class

See full content with helpful comments here: LuckyBuff.cs

// This buff will create LuckyBuffInstance, a timed buff.
// Since LuckyBuffInstance is not a BuffInstance<TValue, TBuff>, we cannot use SimpleValueBuff but only SimpleBuff
// We then make our own `CreateInstance` that was provided by SimpleValueBuff.
public class LuckyBuff(ISingletonLoader loader, IBuffService buffs, ILoc t, EventBus eb) : SimpleBuff(loader, buffs), IUnloadableSingleton
{
    // ...
}
LuckyBuffInstance CreateInstance(float hours, float perc)
{
    // Do not just create an instance directly, you need to inject the services
    // And values if it's of IValuedBuffInstance
    return buffs.CreateBuffInstance<LuckyBuff, LuckyBuffInstance, LuckyBuffInstanceValue>(this, new(hours, perc));
}
[OnEvent]
public void OnDaytimeStart(DaytimeStartEvent _)
{
    const float MaxBuffPerc = .2f; // Only 20% compared to other buffs' 100%
    const float MaxBuffHours = 30f; // There is a chance two or more buffs exist at once

    // We do not remove existing instance like the other buffs

    // Now we create a random boost for a random amount of hours
    var hours = UnityEngine.Random.Range(0, MaxBuffHours);
    var perc = UnityEngine.Random.Range(0, MaxBuffPerc);

    // Lucky buff is applied with positive percentage
    var instance = CreateInstance(hours, perc);
    buffs.Apply(instance);

    Debug.Log($"Lucky buff applied with {perc:P} speed increased for {hours:F1} hours");
}

Other parts of the code are similar to PositiveBuff so I will not repeat them here.

4.2 Create LuckyBuffInstance class

See full content with helpful comments here: LuckyBuffInstance.cs

// Since this BuffInstance needs a few values, we create a record to hold them.
public readonly record struct LuckyBuffInstanceValue(float Hours, float Speed);
// This BuffInstance is timed so we use TimedBuffInstance which already has the logic for processing it
// We also implement IValuedBuffInstance<LuckyBuffInstanceValue> because TimedBuffInstance doesn't support it out of the box.
public class LuckyBuffInstance : TimedBuffInstance<LuckyBuff>, IValuedBuffInstance<LuckyBuffInstanceValue>
{
    // Property for IValuedBuffInstance<LuckyBuffInstanceValue> so the IBuffService can set it automatically
    public LuckyBuffInstanceValue Value { get; set; }

    // ...
}
// This is the total time when the buff starts. Make sure it's available before Init() call of the base class.
public override float StartingTime => Value.Hours;

// If you override AdditionalDescription, note that the base class already has a logic to show the time left.
// public override string? AdditionalDescription { get => base.AdditionalDescription; protected set => base.AdditionalDescription = value; }

![TIP]
Unless you have special logic, saving StartingTime is not necessary because the base class already saves and loads the remaining time for you.

// These are the dependencies that the TimedBuffInstance needs to process it,
// so we need to inject them as well beside what extra you may need
protected override IBuffService Buffs { get; set; } = null!;
protected override IDayNightCycle DayNight { get; set; } = null!;
protected override ILoc T { get; set; } = null!;

IBuffableService buffables = null!;
EventBus eb = null!;

[Inject]
public void InjectDeps(IBuffService buffs, IDayNightCycle dayNight, ILoc t, IBuffableService buffables, EventBus eb)
{
    // Beside injecting your own dependencies, you need to inject the base class dependencies as well
    base.Inject(buffs, dayNight, t);

    this.buffables = buffables;
    this.eb = eb;
}

Other parts of the code are similar to PositiveBuffInstance so I will not repeat them here.

4.3 Register the Buff

In your mod configuration, register the buff like the other buffs:

public override void Configure()
{
    // ...

    Bind<LuckyBuff>().AsSingleton();
}

4.4 Test the Buff

Now load the game again, you should see the lucky buff applied to the beavers every day. The buff will last for a random amount of time between 0 to 30 hours.

Lucky Buff

This buff’s bonus should stack with the positive buff and the negative buff you created.

You can also try saving the game, then reloading it to make sure the value and remaining time is saved and loaded correctly.