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
- Copy
ul_BoilerplateintoMods/and rename the copy,yourname_mymodfor example.ul_is reserved for the official mods. - 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 }
}
- Fill in the
.ulmodfile (Anatomy of a mod) and replacethumbnail.png. - Search the folder for
PLACEHOLDERand[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'stooltip(Choices). - Delete what you do not need. A scene can go as long as nothing names it any more: its
.ulsocialentry, theSCENE_ALWAYSof its action. - 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'sBoilerplate.npc(Game)) is true in an NPC scene. Check it beforeGame:Log,faceEachOther,startQuest,setActorAlias,Outcomes,Agenda,Events,Memory,addBuff, the camera or the HUD. EvenGame:Logrefuses an NPC scene. - Who is who.
Playeris the NPC who runs the scene;Actoris the other NPC ("target": "nearby") or nobody ("self").rapportwithplayeron 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'smaximumParticipantsallows. 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, andActor:GetTokenonly knowsname,he,himandhis. Build shared lines with:name()andGetGenderedWord. 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
.ulsocialchoices. 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:onis ignored there, butEvents:handlerrefuses the scene, so keep event code in a file of its own. - Actions NPCs may take have an
NPCblock and none ofWHERE = { "interaction" },WHO_LUA,AVAILABLE,END_PARTY,OUTDOOR,WEATHER, aBUFFeffect, anInteracting:orrapportwithplayereffect. TheirSCENE_ALWAYSmust be declared in a.ulsocial. Interactions stay the player's: NPCs reach the same scene through the.ulsocialinstead, asul_bp_talkandul_bp_groupdo. - 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_encounterruns a scene at startup (Testing). A scene started on purpose like this hasGame:ForcedTrigger()true, which the encounter uses to skip its dice.- The Mods panel page has a
[TEST]button that plays the encounter.