Table of Contents

Updating to Nautilus

In this article, we will be talking about the necessary changes you must apply to update your mod to Nautilus from SMLHelper 2.0.

Namespace

The root namespace for Nautilus is not the same as SMLHelper 2.0.

- <RootNamespace>SMLHelper.V2</RootNamespace>
+ <RootNamespace>Nautilus</RootNamespace>

Referencing

In BepInEx, we no longer use a mod.json file to determine dependencies. Instead, this is done via the [BepInDependency] attribute. You can mark Nautilus as a dependency to your mod by simply adding the [BepInDependency("com.snmodding.nautilus")] attribute at the top of your entry point class. Example

For more information on the new development setup, please visit our Development Setup Guide.

Handlers

Handlers no longer implement an interface matching their name. Additionally, they're now public static.
This means they also no longer have a public static Main property anymore, so you will have to drop it from anywhere mentioned in your code.

Handler.cs

Following the handler interfaces change, the overly under-used Handler class will leave us in Nautilus

// Handler.cs

- namespace SMLHelper.V2
- {
-     using Interfaces;
- 
-     /// <summary>
-     /// A simple location where every SMLHelper handler class can be accessed.
-     /// </summary>
-     public static class Handler
-     {
-         public static IBioReactorHandler BioReactorHandler => Handlers.BioReactorHandler.Main;
- 
-         public static ICraftDataHandler CraftDataHandler => Handlers.CraftDataHandler.Main;
- 
-         public static ICraftTreeHandler CraftTreeHandler => Handlers.CraftTreeHandler.Main;
- 
-         public static IIngameMenuHandler IngameMenuHandler => Handlers.IngameMenuHandler.Main;
-         ...
-     }

BioReactorHandler

The BioReactorHandler class is removed in Nautilus because it only had one very simple method to patch, and was forcing patch-time. That means if you tried to modify a bio charge after SML's entry point, it didn't get applied.

The following example demonstrates how you can implement the same functionality the BioReactorHandler class offered.

- BioReactorHandler.SetBioreactorCharge(TechType.Peeper, 69f);
+ BaseBioReactor.charge[TechType.Peeper] = 69f;

FishHandler

Ever since this class has been added, it never received any further updates due to unpopularity among modders, and unfamiliarity with how creatures worked in general and thus, has been broken for a long time.
The FishHandler has been removed in Nautilus. At the time being, we have not added a system to replace it, so stay tuned for that.

PDAEncyclopediaHandler And PDALogHandler

Beginning with Nautilus, both of these handler methods were moved to PDAHandler as they only had one method each.

PDAEncyclopedia.EntryData entry = new PDAEncyclopedia.EntryData()
{
  key = "SomeEncy",
  path = "Tech/Tools",
  nodes = new[] { "Tech", "Tools" }
};

- PDAEncyclopediaHandler.AddCustomEntry(entry);
+ PDAHandler.AddEncyclopediaEntry(entry);

- PDALogHandler.AddCustomEntry("SomeLog", "SomeLanguageKey");
+ PDAHandler.AddLogEntry("SomeLog", "SomeLanguageKey");

InGameMenuHandler

The methods InGameMenuHandler class had have been moved to the Nautilus.Utility namespace and the class has been renamed to SaveUtils.

- InGameMenuHandler.RegisterOnSaveEvent(() => ErrorMessage.AddMessage("We do be saving!"));
+ SaveUtils.RegisterOnSaveEvent(() => ErrorMessage.AddMessage("We do be saving!"));

Enum Handlers

Beginning with Nautilus, enum handling will be made generic. Instead of working with individual handlers associated with the enum type (E.G: For TechType we had TechTypeHandler), Now you can add a new enum value to any enum type by using EnumHandler.AddEntry<TEnum>(string enumName).

This means the following handlers are removed:

  • TechTypeHandler
  • BackgroundTypeHandler
  • EquipmentHandler
  • PingTypeHandler
  • TechCategoryHandler
  • TechGroupHandler
  • CraftTreeTypeHandler - Only the methods below are removed:
    1. CreateCustomCraftTreeAndType
    2. ModdedCraftTreeTypeExists

The EnumHandler class contains the following methods, all of them can be used for any enum type:

Signature Summary
EnumBuilder<TEnum> AddEntry<TEnum>(string name) Adds a new enum value instance of TEnum type.
bool ModdedEnumExists<TEnum>(string name) Safely looks for a custom enum object from another mod.
bool TryGetValue<TEnum>(string name, out TEnum enumValue) Safely looks for a custom enum object from another mod and outputs the instance if found.
Note

The EnumHandler class only takes care of registering a new enum object for an enum type. Further configuration is now handled via extension methods for the EnumBuilder type.

Below we will talk about the necessary changes you will need to make your custom enum values work for each of the aforementioned handlers.

Configuring Custom TechType Objects

- TechType customTech = TechTypeHandler.AddTechType("CustomTech", "Custom Tech", "Custom Tech that makes me go yes.", SpriteManager.Get(TechType.Titanium), unlockedAtStart: false);
+ TechType customTech = EnumHandler.AddEntry<TechType>("CustomTech")
+             .WithPdaInfo("Custom Tech", "Custom Tech that makes me go yes.", unlockedAtStart: false)
+             .WithIcon(SpriteManager.Get(TechType.Titanium));

Configuring Custom CraftData.BackgroundType Objects

- CraftData.BackgroundType customBG = BackgroundTypeHandler.AddBackgroundType("CustomBackground", SpriteManager.GetBackground(TechType.Battery));
+ CraftData.BackgroundType customBG = EnumHandler.AddEntry<CraftData.BackgroundType>("CustomBackground")
+             .WithBackground(SpriteManager.GetBackground(TechType.Battery));

Configuring Custom EquipmentType Objects

- EquipmentType customEquipment = EquipmentHandler.AddEquipmentType("CustomEquipment");
+ EquipmentType customEquipment = EnumHandler.AddEntry<EquipmentType>("CustomEquipment");

Configuring Custom PingType Objects

- PingType customPing = PingHandler.RegisterNewPingType("CustomPing", SpriteManager.Get(SpriteManager.Group.Pings, PingType.Signal.ToString()));
+ PingType customPing = EnumHandler.AddEntry<PingType>("CustomPing")
+             .WithIcon(SpriteManager.Get(SpriteManager.Group.Pings, PingType.Signal.ToString()));

Configuring Custom TechCategory and TechGroup Objects

- TechGroup customGroup = TechGroupHandler.AddTechCategory("CustomGroup", "Custom Group");
+ TechGroup customGroup = EnumHandler.AddEntry<TechCategory>("CustomGroup").WithPdaInfo("Custom Group");

- TechCategory customCategory = TechCategoryHandler.AddTechCategory("CustomCategory", "Custom Category");
- TechCategoryHandler.TryRegisterTechCategoryToTechGroup(customGroup, customCategory);
+ TechCategory customCategory = EnumHandler.AddEntry<TechCategory>("CustomCategory").WithPdaInfo("Custom Group")
+             .RegisterToTechGroup(customGroup);

Configuring Custom CraftTree.Type Objects

- ModCraftTreeRoot root = CraftTreeHandler.CreateCustomCraftTreeAndType(CustomTree, out CraftTree.Type customTree);
+ CraftTree.Type customTree = EnumHandler.AddEntry<CraftTree.Type>("CustomTree")
+             .CreateCraftTreeRoot(out ModCraftTreeRoot root);

root.AddTabNode("SomeTab");

Options

The Options system backend was largely changed in Nautilus. This rewrite mostly effects the more in-depth options systems leaving the simplest usage(s) more or less untouched. Mods which made use of the ConfigFile attribute system should not require major changes.

Mods which extended ModOptions to create their config system will require changes:

  • All individual AddXYZOption methods have been replaced by a single unified generic AddItem method.
    • This method takes a ModOption which can be created using ModXYZOption.Create(...).
  • All individual Options_XXYZChanged methods have been replaced by a single unified OnChanged method.
  • Option specific OnChanged events can be added to each option instead of being forced to use the global OnChange.
  • ModChoiceOption has been made into a generic type ModChoiceOption<T> which can support an array of almost any type and enums.

In addition to these Nautilus specific methods there have also been extensions provided to directly create Nautilus OptionItem instances from BepInEx ConfigEntry instances:

var bepInExToggle = cfg.Bind<bool>(
    section: "Testing boolean",
    key: "A boolean",
    defaultValue: true
);
AddItem(bepInExToggle.ToModToggleOption());

SML 2.0

public class ModOptionsV2 : ModOptions
{
    public ModOptionsV2() : base("My Mod Options")
    {
        OptionsPanelHandler.RegisterModOptions(this);

        SliderChanged += Options_SliderChanged;
        ChoiceChanged += Options_ChoiceChagned;
    }

    public override void BuildModOptions()
    {
        AddSliderOption(id: "Foo", label: "Bar", minValue: 0, maxValue: 100, value: 50);
        AddChoiceOption(id: "Baz", label: "Qux", options: new[] { "ABC", "DEF", "XYZ" }, index: 0);
    }

    private void Options_SliderChanged(object sender, SliderChangedEventArgs e)
    {
        switch (e.Id)
        {
            case "Foo":
                // Do stuff here
                break;
        }
    }

    private void Options_ChoiceChagned(object sender, ChoiceChangedEventArgs e)
    {
        switch (e.Id)
        {
            case "Baz":
                // Do stuff here
                break;
        }
    }
}

Nautilus

public class ModOptionsV3 : ModOptions
{
    public ModOptionsV3() : base("My Mod Options")
    {
        OptionsPanelHandler.RegisterModOptions(this);

        OnChanged += GlobalOptions_Changed;

        var sliderWithChange = ModSliderOption.Create(id: "Fancy", label: "Slider", minValue: 0, maxValue: 100, value: 50);
        sliderWithChange.OnChanged += specific_OnChanged;
        AddItem(sliderWithChange);

        AddItem(ModSliderOption.Create(id: "Foo", label: "Bar", minValue: 0, maxValue: 100, value: 50));
        AddItem(ModChoiceOption<string>.Create(id: "Baz", label: "Qux", options: new[] { "ABC", "DEF", "XYZ" }, index: 0));
    }

    private void specific_OnChanged(object sender, SliderChangedEventArgs e)
    {
        // Do onChange here
    }

    private void GlobalOptions_Changed(object sender, OptionEventArgs e)
    {
        switch (e)
        {
            case SliderChangedEventArgs sliderArgs:
                switch (sliderArgs.Id)
                {
                    case "Foo":
                        // Do stuff here
                        break;
                }
                break;
            case ChoiceChangedEventArgs<string> choiceArgs:
                switch (choiceArgs.Id)
                {
                    case "Baz":
                        // Do stuff here
                        break;
                }
                break;
        }
    }
}

Assets

The Assets system received a complete rewrite in Nautilus, making it the biggest change of this version.

With this rewrite, asset classes are no longer an inherited chain mess, meaning Buildable, Craftable, CustomFabricator, Equipable, FishPrefab, PdaItem, Spawnable and ModPrefab classes have been removed.

As of Nautilus, the asset system will have three main parts: Custom Prefabs, Gadgets, and Prefab Templates.

The following table represents all the previous asset classes and what they have been replaced with in Nautilus.

SML 2.0 (old) Nautilus (new)
ModPrefab CustomPrefab
Buildable, PdaItem ScanningGadget
Equipable EquipmentGadget
CustomFabricator FabricatorGadget
Craftable CraftingGadget
Spawnable ICustomPrefab.SetSpawns

Custom Prefabs

CustomPrefab is a class that takes care of registering gadgets and also the game object into the game.
This class is essentially equivalent to the previous ModPrefab class. It is what you will use to actually make a custom prefab.

Gadgets

To put it simply, Gadgets are classes that take certain data and register them to the game for our custom prefab item.
They are pretty much equivalent to the different asset classes and their properties we had before.
Gadgets will be our primary way of interacting with game systems and to add functionality to a tech type and/or class ID.

Note

Gadgets only interact with tech types and/or class IDs. They don't have any business with a prefab's game object.

Prefab Templates

Previously on SML 2.0, asset classes optionally also provided game objects (E.G: CustomFabricator). To allow for diversity in the game object template you choose from and also to make it easier to manage such functionality and modularize game objects, we have moved game object templates to their own system: Prefab templates.

Prefab templates will be our main way of providing a base game object for custom prefabs. There are a couple of options you can choose from that suit your needs, however, it is not enforced to choose one; you can still build up a game object from scratch.

A couple of prefab templates that will be available in Nautilus are the following:

  • CloneTemplate
  • EnergySourceTemplate
  • FabricatorTemplate

Custom Prefab Examples

In this example, we will demonstrate how you can change an SML 2.0 custom prefab to the Nautilus system.

SML 2.0

public class SeamothBrineResistanceModule : Equipable
{
    public static TechType TechTypeID { get; protected set; }
    public SeamothBrineResistanceModule()
        : base("SeamothBrineResistModule",
              "Seamoth brine resistant coating",
              "Makes the Seamoth resistant to corrosive brine pools, by means of a protective coating.")
    {
        OnFinishedPatching += () =>
        {
            TechTypeID = this.TechType;
        };
    }
    public override EquipmentType EquipmentType => EquipmentType.SeamothModule;
    public override TechType RequiredForUnlock => TechType.BaseUpgradeConsole;
    public override TechGroup GroupForPDA => TechGroup.VehicleUpgrades;
    public override TechCategory CategoryForPDA => TechCategory.VehicleUpgrades;
    public override CraftTree.Type FabricatorType => CraftTree.Type.SeamothUpgrades;
    public override string[] StepsToFabricatorTab => new string[] { "SeamothModules" };
    public override QuickSlotType QuickSlotType => QuickSlotType.Passive;
    public override GameObject GetGameObject()
    {
        var prefab = CraftData.GetPrefabForTechType(TechType.SeamothElectricalDefense);
        var obj = GameObject.Instantiate(prefab);
        return obj;
    }
    protected override TechData GetBlueprintRecipe()
    {
        return new TechData()
        {
            craftAmount = 1,
            Ingredients =
            {
                new Ingredient(TechType.Polyaniline, 1),
                new Ingredient(TechType.CopperWire, 2),
                new Ingredient(TechType.AluminumOxide, 2),
                new Ingredient(TechType.Nickel, 1),
            },
        };
    }
}

Nautilus

// Create a custom prefab instance and set the class ID, friendly name, and description respectively
var seamothBrineResistanceModule = new CustomPrefab(
            "SeamothBrineResistModule",
            "Seamoth brine resistant coating",
            "Makes the Seamoth resistant to corrosive brine pools, by means of a protective coating.");
        
// Set our prefab to a clone of the Seamoth electrical defense module
seamothBrineResistanceModule.SetGameObject(new CloneTemplate(seamothBrineResistanceModule.Info, TechType.SeamothElectricalDefense));

// Make our item compatible with the seamoth module slot
seamothBrineResistanceModule.SetEquipment(EquipmentType.SeamothModule)
    .WithQuickSlotType(QuickSlotType.Passive);

// Make the Vehicle upgrade console a requirement for our item's blueprint
ScanningGadget scanning = seamothBrineResistanceModule.SetUnlock(TechType.BaseUpgradeConsole);

// Add our item to the Vehicle upgrades category
scanning.WithPdaGroupCategory(TechGroup.VehicleUpgrades, TechCategory.VehicleUpgrades);

var recipe = new RecipeData()
{
    craftAmount = 1,
    Ingredients =
    {
        new CraftData.Ingredient(TechType.Polyaniline, 1),
        new CraftData.Ingredient(TechType.CopperWire, 2),
        new CraftData.Ingredient(TechType.AluminumOxide, 2),
        new CraftData.Ingredient(TechType.Nickel, 1),
    },
};

// Add a recipe for our item, as well as add it to the Moonpool fabricator and Seamoth modules tab
seamothBrineResistanceModule.SetRecipe(recipe)
    .WithFabricatorType(CraftTree.Type.SeamothUpgrades)
    .WithStepsToFabricatorTab("SeamothModules");

// Register our item to the game
seamothBrineResistanceModule.Register();

This example is based off of a real mod. You can get access to the full source code here.

Audio and FMOD

In the last few versions of SML 2, we made a lot of changes to the audio system SML offered, this was because of FMOD.
FMOD is the sound engine Subnautica uses. It is more advanced and flexible compared to the built-in Unity audio system.

Since we discovered the best practices and better ways to deal with custom sounds, we have deleted a bunch of previously-obsolete methods from CustomSoundHandler and AudioUtils classes, as well as the SoundChannel enumeration in Nautilus.

Beginning with Nautilus, all custom sounds will require a bus instead of a SoundChannel to determine the effects (E.G: reverb, muffling, low-pass, etc..) and the volume slider.
Additionally, the PlaySound signature was also modified and renamed to TryPlaySound.

- Channel channel = AudioUtils.PlaySound(soundPath, SoundChannel.Music);
+ if (AudioUtils.TryPlaySound(soundPath, AudioUtils.BusPaths.Music, out Channel channel))
+ {
+   // do something with channel
+ }


- Channel channel = AudioUtils.PlaySound(soundPath, SoundChannel.Voice);
+ if (AudioUtils.TryPlaySound(soundPath, AudioUtils.BusPaths.PDAVoice, out Channel channel))
+ {
+   // do something with channel
+ }


- Channel channel = AudioUtils.PlaySound(soundPath, SoundChannel.Ambient);
+ if (AudioUtils.TryPlaySound(soundPath, AudioUtils.BusPaths.UnderwaterAmbient, out Channel channel))
+ {
+   // do something with channel
+ }


- Channel channel = AudioUtils.PlaySound(soundPath, SoundChannel.Master);
+ if (AudioUtils.TryPlaySound(soundPath, "bus:/", out Channel channel))
+ {
+   // do something with channel
+ }

Warning

Creating or playing a custom sound on the master bus is il-advised as it is dangerous and has the possibility of breaking the audio for a game session.
Try to set an appropriate bus for your sound instead of the master one.