The SDK is a NuGet package for writing NPCs in C# instead of TOML. It has no dependency on SMAPI or Stardew Valley, so it works in a build script, a console tool, or a SMAPI mod. The runtime is always the normal NPC Helper 2 mod, which players must have installed.
You can use the SDK two ways:
| Output | What you get | Use when |
|---|---|---|
| TOML content pack | A pack of plain files, with no DLL | You want a normal pack but prefer to define the NPC in code. |
| C# DLL mod | A SMAPI mod that writes the same files at startup | Your NPCs are part of a larger C# mod. |
Both produce NPCs the runtime loads identically.
Step 1: Install the package#
In your C# project:
dotnet add package NpcHelper.StardewValley.Sdk
With the .NET 10 SDK or later, dotnet package add NpcHelper.StardewValley.Sdk also works.
You can also install the command-line tool, which uses the SDK.
Step 2: Define an NPC#
using NpcHelper.Sdk;
var rei = new NpcBuilder("Rei")
.DisplayName("Rei the Baker")
.Gender(Gender.Female).Age(AgeGroup.Adult).Manner(Manner.Polite)
.Birthday(Season.Summer, 12)
.Spawn("Town", 10, 12, Facing.Down)
.SpriteFile("art/rei_sprite.png").PortraitFile("art/rei_portrait.png")
.SpriteVariantFile("Hospital", "art/rei_hospital.png")
.Introduction("Hi! I'm Rei.")
.Dialogue("Mon", "Another week.")
.GiftReaction(GiftTaste.Love, "You remembered!")
.Loves("Diamond", "Coffee").Hates("Trash")
.OnWeekdays(s => s.At(600, "Hospital", 1, 18, Facing.Right).At(12, 30, "Town", 29, 54))
.OnWeekends(s => s.At(900, "Beach", 20, 20))
.OnDate(Season.Summer, 12, s => s.At(1000, "Town", 5, 5))
.Spouse(m => m.EngagementAccept("Yes!").OnWeekdays(s => s.At(800, "FarmHouse", 8, 8)))
.Locale("th", l => l.DisplayName("เรย์").Dialogue("Introduction", "สวัสดี"));
Each method matches a setting in the TOML files:
SpriteFile,PortraitFile, andSpriteVariantFiletake image paths and copy them tosprites/andportraits/.- The taste methods, such as
LovesandHates, accept item names, IDs, qualified IDs such as(O)74, category numbers such as-75, and context tags such asegg_item. Dialoguetakes any dialogue key, includingAcceptGift_*keys.Spouse(...)defines engagement lines and the married schedule.Locale(code, ...)adds a translation.
Schedules#
Schedule scopes (together they cover every key the game reads): OnWeekdays, OnWeekends, OnDays(days, stops, season?, hearts?), OnDate(season, day), OnDayOfMonth(day, stops, hearts?), OnRainyDays(stops, secondVariant?), OnGreenRain, OnFestival(id, stops, day?), InSeason, Otherwise, OnKey(key, stops), and RawSchedule(key, value).
Each takes a callback that lists the stops:
| Call | Adds |
|---|---|
At(time, map, x, y, facing?, action?, endMessage?, arriveBy?) |
A stop at a game time such as 600. |
At(hour, minute, map, x, y, ...) |
A stop at a clock time. |
AtSamePlace(time, x, y) |
A stop at a new tile in the same place. |
GoToBed(time) |
The NPC goes to bed. |
StartAt(map, x, y) |
Where the NPC is when the day starts. |
And commands: GoTo(key) or GoTo("season"), NoSchedule(), and UnlessFriendsWith(npc, hearts).
The same scopes exist inside .Spouse(...). For how spouse keys are written, see Marriage.
Heart events and festivals#
new NpcBuilder("Rei")
.HeartEvent("RainyClinic", "Hospital", 4, e => e
.Weather("rainy").Between(900, 1500)
.Farmer(10, 18, Facing.Up).Npc(8, 12, Facing.Down)
.Wait(500)
.Say("You came in out of the rain.$h")
.Walk("Rei", Facing.Down, 2)
.Emote("Rei", "heart"))
.FestivalDialogue("spring13", "I only eat them.", spouseDialogue: "Hold my basket, dear.");
The SDK writes Events.toml and Festivals.toml and reports a bad heart level, an incomplete scene, or a bad festival ID when the NPC is validated. See Heart events and festivals.
Game options#
The SDK enums mirror the game's real option sets: Gender, AgeGroup, Manner, SocialAnxiety, Optimism, Season, HomeRegion, Facing, GiftTaste, NpcLanguage, SocialTabBehavior, CalendarBehavior, EndSlideShowBehavior, and ItemCategory (all 39 categories). On/off options have builder methods too, such as DarkSkinned, CanReceiveGifts, and CountsForPerfection. Game options lists every value.
If your file also has using StardewValley;, Gender and Season become ambiguous with the game's types. Add these aliases:
using Gender = NpcHelper.Sdk.Gender;
using Season = NpcHelper.Sdk.Season;
Step 3: Validate#
Everything is checked as you build.
ValidationReport report = rei.Validate();
Validate() returns a report of errors and warnings. Build() throws an NpcDefinitionException that lists every problem.
Output 1: a TOML content pack#
var result = new NpcPackBuilder("Me.ReiPack", "Rei Pack")
.Author("Me")
.Add(rei)
.WriteTo("out/[HP] ReiPack");
Console.WriteLine(result.Ok ? "valid" : result.Report.ToString());
NpcPackBuildertakes the pack's unique ID and display name.WriteTowritesmanifest.jsonand the standard layout (Pack structure).- It then loads the pack back the way the game does, so
result.Reportshows exactly what the game would say. - Zip it with
npchelper packand publish it (Test and publish).
NpcBuilder.ToToml() returns just the main TOML text if that is all you need.
Output 2: a C# DLL mod#
-
Create the project:
shnpchelper new dll-mod MyMod --id Me.MyMod --npc Rei -
Edit
Npcs.csto define your NPCs. -
Build with
dotnet build.
The generated mod entry gets NPC Helper 2's API and registers each NPC:
var api = Helper.ModRegistry.GetApi<INpcHelperApi>(NpcHelperIds.UniqueId);
api.Register(npc, Path.Combine(Helper.DirectoryPath, "generated"), msg => Monitor.Log(msg, LogLevel.Warn));
Register validates the NPC, writes it in the standard layout under generated/, and registers it with the runtime. The runtime then picks up images, dialogue, schedule, marriage, and locale files from that folder. The mod's manifest lists NPC Helper 2 as a required dependency, so SMAPI loads it first and tells players if it is missing.
What the SDK talks to#
The SDK's INpcHelperApi is a subset of the runtime's Mod API that uses only basic types (strings and string lists), the form SMAPI's API proxy can connect between two separately loaded mods.
Safety#
- Writing never touches a folder it did not create. A non-empty folder that the SDK did not generate is refused (
IOException) unless you passoverwrite: true. - A generated folder is rebuilt from scratch every run, so renamed or removed images do not linger. Edit the C#, not the generated files.
- Names, variant keys, locale codes, and unique IDs are validated, so nothing can write outside the target folder.
- Keep the pack's
ContentPackFor.MinimumVersionat or above what your features need (NpcHelperIds.MinimumVersion).