Skip to content

Pack Format

An NPC pack is a folder under:

…/Schedule I/UserData/Personnel/Packs/<PackName>/
├── manifest.json
└── *.png (optional custom layers, referenced by the manifest)

Since Personnel 2.0 a pack can describe the whole NPC - looks, spawn point, daily schedule, economy role - and Personnel spawns it as a real world NPC on its own (autoRegister). No mod code needed.

{
"name": "My Pack",
"author": "you",
"schemaVersion": 2,
"autoRegister": true,
"npcs": [
{
"id": "mypack_pale",
"name": "Pale",
"appearance": {
"gender": 0.5,
"height": 1.0,
"weight": 0.4,
"skinColor": "#8899AA",
"hairPath": "Avatar/Hair/Spiky/Spiky",
"hairColor": "#101014",
"leftEye": { "top": 0.5, "bottom": 0.5 },
"rightEye": { "top": 0.5, "bottom": 0.5 },
"eyeBallTint": "#FFFFFF",
"faceLayers": [ { "file": "grin.png", "tint": "#FFFFFF" } ],
"bodyLayers": [ { "path": "Avatar/Layers/Top/T-Shirt", "tint": "#334455" } ],
"accessories": [ { "path": "Avatar/Accessories/Feet/Sneakers/Sneakers", "color": "#FFFFFF" } ]
},
"behavior": { "aggression": 0.2, "maxHealth": 100, "scale": 1.0 },
"spawn": {
"x": -66.4, "y": -2.9, "z": 86.1,
"rotationY": 145.0,
"region": "Westville",
"physical": true
},
"contact": { "visible": true, "mapMarker": true },
"relationships": {
"delta": 1.5,
"unlockType": "Recommendation",
"connections": [ "mypack_other_npc", "jessi_waters" ]
},
"customer": {
"spending": { "min": 300, "max": 900 },
"ordersPerWeek": { "min": 1, "max": 3 },
"preferredOrderDay": "Friday",
"orderTime": "19:30",
"standards": "Moderate",
"allowDirectApproach": true,
"guaranteeFirstSample": false,
"mutualRelationRequirement": { "min": 1.0, "max": 4.0 },
"callPoliceChance": 0.05,
"dependence": { "base": 0.2, "multiplier": 1.0 },
"affinities": { "marijuana": 0.6, "cocaine": -0.3 },
"preferredProperties": [ "energizing", "calming" ]
},
"inventory": { "cash": { "min": 20, "max": 120 }, "items": [ "baggie" ], "clearEachNight": true },
"schedule": [
{ "type": "walkTo", "time": "07:30", "position": [-70.1, -2.9, 80.0] },
{ "type": "stayInBuilding", "time": "09:00", "duration": 240, "building": "Thrifty Threads" },
{ "type": "walkTo", "time": "14:00", "position": [-66.4, -2.9, 86.1] }
],
"extensions": { }
}
]
}

Every field is optional - omitted keys keep sensible defaults, so a minimal NPC is still just a name. Colours are #RRGGBB or #RRGGBBAA hex strings. Unknown values are warned about in the log and skipped; a broken field never takes the whole NPC down.

  • An authored id is respected as-is (normalized to lowercase a-z0-9_). Without one, the id is derived as normalize(packId or folder name)_normalize(npcName).
  • Never change an id once the pack shipped - it is the save identity. If you must rename, set saveId to the OLD id so existing saves keep matching.
  • packId (top level) pins the derivation prefix so renaming the pack folder does not change ids.
  • Two NPCs in the same pack must have different ids. The startup log prints each pack’s ids.

Auto-registration (world NPCs without code)

Section titled “Auto-registration (world NPCs without code)”

Set "autoRegister": true at the top level (or "auto": true inside a single NPC’s spawn) and Personnel registers those NPCs as real S1API world NPCs - networked, saved, walking their schedules. Notes:

  • Everyone in a co-op session needs the same packs installed (same rule as mods).
  • Pack changes need a game restart.
  • If a consumer mod already ships a compiled class for the same NPC id, the compiled one wins and the log says so - remove the old DLL once a pack is fully migrated.
  • The EnableAutoRegister setting is a global kill switch.
KeyMeaning
x, y, zSpawn position.
rotationYSpawn yaw in degrees.
regionMap region: Northtown, Westville, Downtown, Docks, Suburbia, Uptown.
physicaltrue = real world body (moves, collides, runs schedules). false = phone contact only - costs next to nothing. Default: physical exactly when the NPC has a schedule.
autoPer-NPC override of the pack’s autoRegister.

Keep most roster NPCs non-physical - that is what keeps big packs fast, also on Steam Deck. For many physical NPCs, Siesta throttles the AI cost.

Stand where the NPC should spawn and run personnel spawn in the dev console to get this whole block with the numbers filled in - see Finding coordinates.

An array of actions; type decides the other fields. Times are "HH:MM" strings, durations are in-game minutes, positions are [x, y, z].

typeFields
walkTotime, position, faceDestination, within, warpIfSkipped
stayInBuildingtime, duration, building (name), doorIndex
sittime, duration, seatSet (name) or seatSetPath, includeInactive, warpIfSkipped
useVendingMachinetime, machineGuid (optional - nearest otherwise)
useAtmtime, atmGuid (optional)
useSlotMachinetime, position, bet, mode (single, spinCount, untilTime, untilBroke, untilTimeOrBroke), spins, endTime, timeBetweenSpins - experimental in multiplayer
locationDialoguetime, position, greetingOverride, choice, plus the walkTo fields
locationActiontime, position, duration, action (smokeBreak, graffiti, drinking, holdItem), equippablePath, graffitiRegion
driveToCarParktime, parkingLot (name), vehicle (name) or createVehicle: { "code": "shitbox", "position": [x,y,z], "rotationY": 0 }
dealSignalno fields - lets a dealer break its schedule for deals

Unknown building/seat names log a warning and the NPC skips that step - schedules keep working when a target is missing.

The game has no coordinate display, so Personnel ships console commands that hand you the spot you are standing on. Turn the console on in Settings (“Console enabled”), press the backtick key, and:

CommandWhat you get
personnel pos[x, y, z] for any field that takes a position
personnel pos 07:30a finished walkTo action for that time
personnel spawna spawn block with x/y/z/rotationY/region filled in
personnel route 07:30appends a walkTo step to UserData/Personnel/route.json
personnel route showprints the collected steps, ready to paste into "schedule": [ ... ]
personnel route clearstarts a new route
personnel npcs [filter]loaded definitions, and where the physical ones are right now

Every result is copied to your clipboard and written to the MelonLoader log. The game’s console has no output pane, so the log (MelonLoader/Latest.log) is where the full text lives; in-game you get a notification confirming the command ran.

Mapping a route is three steps: walk to the first stop, personnel route 07:00, repeat for each stop, then personnel route show and paste the block into the NPC’s schedule.

Two things to watch:

  • NPCs only move on the navmesh. Pick a spot you would walk over yourself - next to a vehicle rather than inside it. Unreachable destinations leave the NPC standing where it was.
  • y does not have to be exact. The game samples the navmesh within about 5 units of the destination, so x and z do the work.

warpIfSkipped: true (added by the commands above) places the NPC at the destination when the step was skipped because no player was nearby at that time. Without it the NPC misses the stop and nothing in the log says so.

Vanilla teleport <property|npc id> gets you to a spot fast - teleport rv, teleport barn, teleport jessi_waters - and then the commands above read that position off.

Without Personnel 2.1.0, the save file has the same numbers: stand on the spot, run save in the console, and read Position from %USERPROFILE%\AppData\LocalLow\TVGS\Schedule I\Saves\<SteamID>\SaveGame_1\Players\Player_0\Player.json.

Presence of a customer or dealer block gives the NPC that economy role (a dealer block wins if both exist; the old behavior.conversation shorthand still works). Only the fields you set are applied - everything else keeps the game’s defaults.

  • customer: spending, ordersPerWeek, preferredOrderDay (Monday..Sunday), orderTime, standards (VeryLow, Low, Moderate, High, VeryHigh), allowDirectApproach, guaranteeFirstSample, mutualRelationRequirement, callPoliceChance, dependence, affinities (drug type -> -1..1: marijuana, methamphetamine, cocaine, mdma, shrooms, heroin), preferredProperties (property ids like energizing).
  • dealer: type (player or cartel), cut, signingFee, home (building name), completedDealsVariable, allowInsufficientQuality, allowExcessQuality.
  • contact.mapMarker: false removes the phone-map marker. contact.visible: false (experimental) skips the contact unlock, so the NPC shows as ”???”.
  • relationships: delta (0-5), unlocked, unlockType (Recommendation or DirectApproach), connections (NPC ids - pack-prefixed for pack NPCs, plain vanilla ids like jessi_waters work too).

Each entry in faceLayers / bodyLayers / accessories uses one source:

KeyMeaning
pathAn existing in-game layer / accessory Resources path (what Personify’s pickers store).
fileA pack-relative PNG registered as a custom layer at load time - see Custom PNG Layers.
tint (alias color)Hex tint applied to the layer. Default white.

Face layers render on the face mesh, body layers on the body mesh - a custom file in faceLayers is registered as a face layer, in bodyLayers as a body layer. Accessories are attached meshes and only take path (custom accessory meshes are not supported).

Free-form blocks keyed by consumer name (e.g. "backrooms": { ... }). Personnel passes them through untouched; each consumer parses its own block and ignores the rest.

A pack is just files, so ship it any way you like. Personify exports the pack already wrapped Thunderstore-style (manifest.json/README/LICENSE + your pack under Personnel/Packs/<name>/); add a 256x256 icon.png and upload. List DooDesch-Personnel as a dependency.