Getting started

The boilerplate mod

ul_Boilerplate, a complete mod with placeholders everywhere, to copy and rename: scenes that start by themselves, place, person and group actions, a quest, and the NPC side.

Your first mod is three small files. A real mod quickly needs much more: scenes that start on their own, actions at places and on people, a quest, a settings page, and above all scenes the town's NPCs can play too. ul_Boilerplate is all of that, wired together and working, with every line of text left as a placeholder.

It ships with the game in Content/Modules/_Templates/ul_Boilerplate/. The game never loads it: only the folders directly under Content/Modules/ and Mods/ are mods, and _Templates is not one. It is also in the official mods repository, under _Templates/.

Start your mod from it

  1. Copy ul_Boilerplate into Mods/ and rename the copy, yourname_mymod for example. ul_ is reserved for the official mods.
  2. Replace the names inside it. In this order, case-sensitive:
Replace With (example) What it is
ul_Boilerplate yourname_mymod The folder name, the mod's identity
Boilerplate MyMod The global table of Lua/ul_bp.lua
ul_bp ymm The prefix of every id, file, variable, global and setting
UlBp Ymm Building aliases (UlBpVenue)
ulBp ymm Methods added to every person (Actor:ulBpProvoke)

From PowerShell, inside the copied folder:

text$pairs = @(@('ul_Boilerplate','yourname_mymod'), @('Boilerplate','MyMod'), @('ul_bp','ymm'), @('UlBp','Ymm'), @('ulBp','ymm'))
Get-ChildItem -Recurse -File | Where-Object { $_.Extension -ne '.png' } | ForEach-Object {
  $t = [IO.File]::ReadAllText($_.FullName)
  foreach ($p in $pairs) { $t = $t.Replace($p[0], $p[1]) }
  [IO.File]::WriteAllText($_.FullName, $t)
}
Get-ChildItem -Recurse | Sort-Object { $_.FullName.Length } -Descending | ForEach-Object {
  $n = $_.Name; foreach ($p in $pairs) { $n = $n.Replace($p[0], $p[1]) }
  if ($n -ne $_.Name) { Rename-Item -LiteralPath $_.FullName -NewName $n }
}
  1. Fill in the .ulmod file (Anatomy of a mod) and replace thumbnail.png.
  2. Search the folder for PLACEHOLDER and [PH]: every hit is text for you to write. [PH] marks choice labels, which the game draws as petals around the scene: keep yours under about 22 characters and put the detail in the option's tooltip (Choices).
  3. Delete what you do not need. A scene can go as long as nothing names it any more: its .ulsocial entry, the SCENE_ALWAYS of its action.
  4. Remove the [TEST] button of the Mods page before you publish.

What is in it

File Shows Read
Lua/ul_bp.lua One global table, player settings, helpers that work for the player and for NPCs, money through the Ledger, a soft dependency on ul_Core, AVAILABLE functions, methods added to every person Libraries
Lua/ul_bp_events.lua Events:on, saved calls (Events:handler + Events:schedule), notifications, phone texts, queueScene Game-time events, Notifications
Stats/, History/, Witness/, Greetings/ A hidden stat with daily decay; life events and a backstory; a witnessed act type; click greetings Stats, History, Witnesses, Greetings
Actions/ul_bp/ul_bp_venue.ulaction An action at a place, with an NPC block so NPCs take it too Actions, NPC life
Actions/ul_bp/ul_bp_talk.ulaction An action on a person: INTENT, WHO_LUA, AVAILABLE, Interacting: effects Interactions
Actions/ul_bp/ul_bp_group.ulaction An action on a group: CONVERSATION = { MIN = 3, MAX = 5 } Conversations
Actions/phone/ul_bp_text.ulaction A phone action, hidden until it makes sense Actions
Scenes/ul_bp_encounter.ulscene A scene that starts by itself, for the player (random roll) and for NPCs (ambient) Scenes
Scenes/ul_bp_venue.ulscene The place action's scene, player and NPC: an agenda meeting, a witnessed act, odds, a buff Agenda
Scenes/ul_bp_talk.ulscene The person action's scene, player and NPC: an outcome, odds from stats, personality Outcomes
Scenes/ul_bp_group.ulscene Several people at once: the player's conversation, or an NPC's group Conversations
Scenes/ul_bp_rumour.ulscene NPC only: what someone saw spreads through the town Witnesses
Scenes/ul_bp_wake_up.ulscene Starts on waking; follows up yesterday's talks Outcomes
Scenes/ul_bp_callback.ulscene Queued by a saved call; a phone call; story memory State and memory
Scenes/ul_bp_text.ulscene A card inside a scene (showPanel), a plan in the agenda Mod UI
Quests/ul_bp_story/ A three-step quest: building aliases, objectives, three endings Quests
SocialSimulation/ul_bp.ulsocial Which scenes NPCs play and how they choose; town-wide globals; a life choice NPC life
AI/ul_bp_regulars.lua A routine: where the people the mod marked spend their evenings NPC routines
Menu/ul_bp_page.lua, Map/Hud/ul_bp_hud.lua A Mods-panel page (status, settings, buttons) and a piece of HUD Mod UI

Every scene offers at least three answers at its main choice, and most open a second choice. Every file explains itself in its comments.

One scene for the player and for NPCs

The town's NPCs play the same scene files as the player, through the declarations of a .ulsocial file (NPC life). They do it in a separate, smaller API, and a call that API does not have refuses the whole NPC scene: nothing it did is kept. The boilerplate's scenes follow these rules:

  • Branch before player-only calls. Game:isNpcSimulation() (the boilerplate's Boilerplate.npc(Game)) is true in an NPC scene. Check it before Game:Log, faceEachOther, startQuest, setActorAlias, Outcomes, Agenda, Events, Memory, addBuff, the camera or the HUD. Even Game:Log refuses an NPC scene.
  • Who is who. Player is the NPC who runs the scene; Actor is the other NPC ("target": "nearby") or nobody ("self"). rapportwithplayer on Actor is what Actor thinks of that NPC.
  • Nobody is created. In an NPC scene Game:generatePerson() takes no arguments: the first call returns Actor, each next one someone else already at the place, as many times as the scene's maximumParticipants allows. When nobody fits, the scene is refused, which only means it does not happen this time.
  • Text. <Actor.name> tokens are not filled in for NPCs, and Actor:GetToken only knows name, he, him and his. Build shared lines with :name() and GetGenderedWord. Narration is the player's first person ("I…"), so give NPCs a third-person line or none.
  • Every option an NPC may see needs a rule in the .ulsocial choices. Give each choice point of a scene its own range of indices (0-3, 10-12, 20-23) and "point": 0 (any choice point) stays unambiguous.
  • Libraries. An NPC scene only has the libraries its entry lists. A library listed there must only define things: Events:on is ignored there, but Events:handler refuses the scene, so keep event code in a file of its own.
  • Actions NPCs may take have an NPC block and none of WHERE = { "interaction" }, WHO_LUA, AVAILABLE, END_PARTY, OUTDOOR, WEATHER, a BUFF effect, an Interacting: or rapportwithplayer effect. Their SCENE_ALWAYS must be declared in a .ulsocial. Interactions stay the player's: NPCs reach the same scene through the .ulsocial instead, as ul_bp_talk and ul_bp_group do.
  • Scenes under a Quests/ folder are player-only.

Test it

  • From the game's source repository, python Tools/ModDocs/validate_mod.py <your mod folder> checks every API call, id and venue tag against the game. It is also what to paste back to an assistant (Modding with Claude): the boilerplate is a good first file to give one.
  • -WorldMapScene=ul_bp_encounter runs a scene at startup (Testing). A scene started on purpose like this has Game:ForcedTrigger() true, which the encounter uses to skip its dice.
  • The Mods panel page has a [TEST] button that plays the encounter.