# Modding API

> Ship content through CultTweaker with no code, or reference CultTweakerApi for queries, actions, quests and content paths.

There are two ways to build on CultTweaker: ship content as files and write no
code at all, or reference the assembly and call a small contract. Most mods want
the first.

## Shipping content with no code

Every content folder is read from two places: CultTweaker's own plugin folder,
and a folder named `CultTweaker` inside any other mod's folder. Put your files
there and they load beside the player's own, with no reference to our assembly
and no code from you:

```text
BepInEx/plugins/YourMod/CultTweaker/CustomDungeonMaps/YourDungeon.json
BepInEx/plugins/YourMod/CultTweaker/CustomLevelBlueprints/YourLevel.json
BepInEx/plugins/YourMod/CultTweaker/CustomNodeBlueprints/YourRoom.json
BepInEx/plugins/YourMod/CultTweaker/CustomNpcs/YourNpc/config.json
BepInEx/plugins/YourMod/CultTweaker/CustomEnemies/YourEnemy/config.json
BepInEx/plugins/YourMod/CultTweaker/PlayerSkins/YourSkin/config.json
```

The search is a bounded walk three folders deep, so a nested install is still
found. Three rules apply:

- Reading is shared but writing is not, so your files are never edited in place.
- Where two mods use the same name, the player's own copy wins and the other is
skipped with a warning.
- Everything the editors save goes to CultTweaker's own folder, never into yours.

## The code contract

One class: `CustomSpineLoader.Api.CultTweakerApi`, in `CultTweaker.dll`.
Everything else in the assembly is internal in spirit and gets rearranged without
notice. Members here are never removed or changed in meaning; new ones are added
and `ContractVersion` is raised.

Reference the DLL with `Private="false"` so you do not ship a copy of it:

```xml
<Reference Include="CultTweaker">
  <HintPath>lib\CultTweaker.dll</HintPath>
  <Private>false</Private>
</Reference>
```

## Reaching it safely

Declare a soft dependency, probe before the first call, and keep every call to
our types inside a method marked no-inlining so the JIT never loads them when
CultTweaker is absent:

```csharp
[BepInDependency("InfernoDragon0.cotl.CustomSpineLoader", BepInDependency.DependencyFlags.SoftDependency)]
public class YourPlugin : BaseUnityPlugin
{
    private static bool _available;

    private void Awake()
    {
        _available = Chainloader.PluginInfos.ContainsKey("InfernoDragon0.cotl.CustomSpineLoader")
                     && Probe();
        if (_available) Bridge.WhenReady();
    }

    private static bool Probe()
    {
        try
        {
            var type = Type.GetType("CustomSpineLoader.Api.CultTweakerApi, CultTweaker");
            return type != null && CultTweakerApi.ContractVersion >= 1;
        }
        catch (Exception) { return false; }
    }
}

internal static class Bridge
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    public static void WhenReady() => CultTweakerApi.OnReady(() =>
    {
        foreach (var name in CultTweakerApi.Names(CultTweakerApi.Kind.Dungeons))
            Log.LogInfo("CultTweaker dungeon available: " + name);
    });
}
```

<note>

Nothing throws out of the contract. A query for a kind this build does not know
logs a warning and returns an empty list.

</note>

## Content kinds

Pass one of these to the queries. The constants are on `CultTweakerApi.Kind`.

<table>
<thead>
  <tr>
    <th>
      Kind
    </th>
    
    <th>
      What it lists
    </th>
    
    <th>
      Folder
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Dungeons
      </code>
    </td>
    
    <td>
      Playable custom dungeons, by internal name
    </td>
    
    <td>
      none, registered at runtime
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        DungeonMaps
      </code>
    </td>
    
    <td>
      Dungeon map documents
    </td>
    
    <td>
      <code>
        CustomDungeonMaps
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Levels
      </code>
    </td>
    
    <td>
      Level documents
    </td>
    
    <td>
      <code>
        CustomLevelBlueprints
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Rooms
      </code>
    </td>
    
    <td>
      Room blueprint documents
    </td>
    
    <td>
      <code>
        CustomNodeBlueprints
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        WorldMaps
      </code>
    </td>
    
    <td>
      World map documents
    </td>
    
    <td>
      <code>
        CustomWorldMaps
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        MainMenus
      </code>
    </td>
    
    <td>
      Main menu presets
    </td>
    
    <td>
      <code>
        CustomMainMenus
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Npcs
      </code>
    </td>
    
    <td>
      Registered custom NPCs
    </td>
    
    <td>
      <code>
        CustomNpcs
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Enemies
      </code>
    </td>
    
    <td>
      Registered custom enemies
    </td>
    
    <td>
      <code>
        CustomEnemies
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Structures
      </code>
    </td>
    
    <td>
      Registered custom structures
    </td>
    
    <td>
      <code>
        CustomStructures
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Items
      </code>
    </td>
    
    <td>
      Registered inventory items
    </td>
    
    <td>
      <code>
        CustomInventoryItems
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Meals
      </code>
    </td>
    
    <td>
      Registered meals
    </td>
    
    <td>
      <code>
        CustomMeals
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Tarots
      </code>
    </td>
    
    <td>
      Registered tarot cards
    </td>
    
    <td>
      <code>
        CustomTarotCards
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Weapons
      </code>
    </td>
    
    <td>
      Custom weapons, as <code>
        spineFolder/weaponName
      </code>
    </td>
    
    <td>
      <code>
        PlayerSkins
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        PlayerSkins
      </code>
    </td>
    
    <td>
      Player spines, as <code>
        spineFolder/skinName
      </code>
    </td>
    
    <td>
      <code>
        PlayerSkins
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        FollowerSkins
      </code>
    </td>
    
    <td>
      Custom follower skins
    </td>
    
    <td>
      <code>
        FollowerSkins
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Cutscenes
      </code>
    </td>
    
    <td>
      Cutscene videos
    </td>
    
    <td>
      <code>
        CustomCutscenes
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ShapeProfiles
      </code>
    </td>
    
    <td>
      Sprite shape profiles
    </td>
    
    <td>
      <code>
        CustomShapeProfiles
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        BuildingOverrides
      </code>
    </td>
    
    <td>
      Buildings with overridden art
    </td>
    
    <td>
      <code>
        BuildingOverrides
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Quests
      </code>
    </td>
    
    <td>
      Quests declared by custom NPCs, as <code>
        npcInternalName/questId
      </code>
    </td>
    
    <td>
      <code>
        CustomNpcs
      </code>
    </td>
  </tr>
</tbody>
</table>

`CultTweakerApi.Kinds()` returns the whole list, so you can iterate without
hard-coding it, and `FolderFor(kind)` gives the folder name, or null for a kind
with no folder.

## Queries

```csharp
IReadOnlyList<string> Names(string kind);   // what this install has
bool                  Has(string kind, string name);
int                   IdOf(string kind, string name);   // see the warning below
string                FolderFor(string kind);
IReadOnlyList<string> Kinds();
bool                  Ready { get; }
string                Version { get; }
void                  OnReady(Action callback);
```

Registered kinds answer with what actually loaded. Document kinds answer with the
file names found across every mod's folders, including yours.

## Actions

```csharp
bool       EnterDungeon(string name);   // by internal name or in-game name
string     CurrentDungeon();            // internal name, or null
GameObject SpawnNpc(string name, Vector3 position, Transform parent = null);
```

`EnterDungeon` starts a run the way the player entering it would, so call it from
a normal gameplay moment rather than during a load. It returns false when this
install does not have that dungeon.

## Quests (contract version 2)

```csharp
string                QuestState(string key);   // notStarted, active, ready, done, failed, or null
IReadOnlyList<string> ActiveQuests();
bool                  GiveQuest(string key);
bool                  TurnInQuest(string key);
bool                  AbandonQuest(string key);
void                  NoteQuestEvent(string flag);
```

Keys are the names `Names(Kind.Quests)` returns: `npcInternalName/questId`.
`ready` means every goal is met and the player has not handed the quest in yet.

`NoteQuestEvent` is the hook for a quest that has to wait on something the game
never announces: declare a goal of type `flag` with a name of your choosing, and
raise that name when your own mod decides the moment has come. Quests, their
goals and their text are written by whoever makes the NPC - see
[custom NPC quests](/docs/culttweaker/custom-npc-quests) - so a mod can ship a
quest with no code at all and only reach for these calls when it needs to drive
one.

Quest progress is CultTweaker's own per-slot file, not the game's save, and it is
keyed by name throughout, so none of this carries the id warning below.

## Where content lives

```csharp
string                ContentRoot(string folderName);        // CultTweaker's own folder
IReadOnlyList<string> ContentRoots(string folderName);       // ours, then every mod's
IReadOnlyList<string> ContentDirectories(string folderName); // per-item folders, absolute
IReadOnlyList<string> ContentFiles(string folderName, string pattern);
string                FindContentFile(string folderName, string fileName);
string                FindContentDirectory(string folderName, string subFolder);
```

Use these to find your own shipped content on disk, or to read a document a
player made. Do not write into `ContentRoot`: that folder holds the player's own
creations. Ship your files inside your own plugin folder as shown at the top.

## Ids and why you must not save them

`IdOf` returns the number the game gave a piece of content, for the kinds backed
by a game enum: items, meals, tarots, structures, enemies, weapons and dungeons.

<warning>

**These numbers are allocated per install, in load order.** The same dungeon is a
different number on another machine, and adding or removing any mod can renumber
everything. Use an id for an immediate call into the game and nothing else. Never
write one to a save file and never send one to another machine: send the name and
call `IdOf` again on the other side.

</warning>

This is not theoretical. A multiplayer mod sent a custom dungeon's raw id to the
other player, whose install had a different number for it, and the guest sat on a
black loading screen because the dungeon it was told to load did not exist there.

## Load order

CultTweaker registers its content during its own `Awake`, and BepInEx does not
promise plugin order. A mod that reads the registries from its own `Awake` may
see nothing. Either read them lazily, when a scene loads or when the player does
something, or pass a callback to `CultTweakerApi.OnReady`, which runs immediately
if content is already loaded and at the end of our boot otherwise.

## Versioning

`CultTweakerApi.ContractVersion` started at 1 and goes up by one whenever members
are added. Members are never removed and never change meaning, so code written
against 1 keeps working against 2. Check it once at startup if you need a member
added later:

```csharp
if (CultTweakerApi.ContractVersion >= 2) { /* the quest members */ }
```

<table>
<thead>
  <tr>
    <th>
      Version
    </th>
    
    <th>
      Added
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      1
    </td>
    
    <td>
      The kinds, the queries, the actions, the content paths, <code>
        OnReady
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      2
    </td>
    
    <td>
      <code>
        Kind.Quests
      </code>
      
       and the quest members.
    </td>
  </tr>
</tbody>
</table>

If you need something the contract does not expose, ask rather than reflecting
into the assembly: anything reached by reflection will break the next time those
internals move.
