NPC schedules
Status: experimental.
What it is
The game gives its NPCs a weekly timetable: at certain days and hours an NPC goes to a place and does something, such as working, eating or sleeping. This API lets a mod read what an NPC is doing right now, read its whole weekly timetable, and get told when an NPC starts a different activity. It only reads. It does not move NPCs. To move an NPC around, use NPC movement.
NPCs are given by their id. You get the ids from the NPC API (NpcInfo.Id, Npcs.All()). This API does not list NPCs itself.
NPCs that your own mod spawned have no timetable of the game’s kind. To give those a timetable, use Mod NPC routines.
When to use it
- Show or log what an NPC is doing, for example “Sam is at work”.
- React when an NPC starts or stops an activity.
- Work out where an NPC will be at a certain time by reading its timetable.
Quick example
This mod writes a log line each time any NPC changes activity. Once a save is loaded, it also logs what the first NPC the game knows is doing, and how many entries that NPC’s timetable has.
using ObenseuerCore;
[ModInfo("example.schedulewatch", "Schedule Watch", "0.1.0")]
[ModDependency("obenseuer.moddingframework", "0.11.0")]
public sealed class ScheduleWatch : FrameworkMod
{
protected override void OnLoad()
{
NpcSchedules.ActivityChanged += args => Log.Info("NPC " + args.NpcId + " went from " + (args.From ?? "nothing") + " to " + (args.To ?? "nothing"));
Events.WhenReady(() =>
{
foreach (NpcInfo npc in Npcs.All())
{
Log.Info(npc.Name + " is doing " + (NpcSchedules.CurrentActivity(npc.Id) ?? "nothing") + ", timetable entries: " + NpcSchedules.Timetable(npc.Id).Count);
break;
}
});
}
}
ActivityChanged is an event: the framework calls your code each time it happens. From and To are null when the NPC had no activity before, or has none now, which is why the example writes “nothing” in their place. WhenReady runs your code once a save is loaded (see Using events).
Common tasks
Find out what one NPC is doing now. You get null when the NPC has no activity or the id is not known.
string activity = NpcSchedules.CurrentActivity(npcId);
Print an NPC’s whole timetable.
foreach (ScheduleInfo entry in NpcSchedules.Timetable(npcId))
Log.Info(entry.Name + ": " + entry.ActivityName + " at " + entry.Place + ", day " + entry.WeekDay + ", from hour " + entry.StartHour + " for " + entry.DurationMinutes + " minutes");
Check that the change event works before you rely on it.
if (!NpcSchedules.IsAvailable) Log.Warn("Activity changes are not reported");
Reference
This is the full list of what the API gives you, one section for each part. Text in brackets at the end of a note is extra detail for experienced modders, and you can skip it on a first read.
Schedules
NpcSchedules reads an NPC’s current activity and its timetable. Each timetable entry is a ScheduleInfo.
NpcSchedules read an NPC's activity and timetable
Call CurrentActivity(npcId) to see what the NPC is doing now, and Timetable(npcId) for its fixed timetable.
public static class NpcSchedules
{
public static bool IsAvailable { get; }
public static string CurrentActivity(int npcId);
public static List<ScheduleInfo> Timetable(int npcId);
}
| Method or property | Returns | What it does |
|---|---|---|
IsAvailable | bool | false when the activity change event could not be set up. |
CurrentActivity(int npcId) | string | What the NPC is doing now. null when it has no activity. |
Timetable(int npcId) | List<ScheduleInfo> | The NPC’s timetable. An empty list when the NPC is not known. |
Things to know
CurrentActivity(npcId)tells you what the game says the NPC is doing, even when the NPC is not in the scene you are in. You getnullwhen the NPC has no activity or no NPC has that id. (it reads the current activity of the NPC’s game data. The name is the activity’s name, or the activity’s class name when the activity has no name.)Timetable(npcId)lists the fixed timetable in the order the game stores it.ActivityNameis the activity’s name, or the name of its code class when it has no name. It isnullwhen the entry has no activity.IsAvailableisfalsewhen the activity change event could not be set up. The reads keep working when it isfalse. See If something fails to set up.
Limits
Timetable(npcId)does not include random activities that the game picks on its own.Timetable(npcId)gives an empty list when the id is not known.
ScheduleInfo one entry in an NPC's timetable
Timetable(npcId) gives you one of these for each entry. StartHour, DurationMinutes and Place tell you when and where the NPC is due.
public sealed class ScheduleInfo
{
public string Name { get; }
public string ActivityName { get; }
public string Place { get; }
public int WeekDay { get; }
public float StartHour { get; }
public float DurationMinutes { get; }
public bool Disabled { get; }
}
| Property | Type | What it means |
|---|---|---|
Name | string | The entry’s name in the game’s data. |
ActivityName | string | What the NPC does. null when the entry has no activity. |
Place | string | The scene the activity takes place in. null when unknown. |
WeekDay | int | The day of the week the entry starts on, as the game numbers it. |
StartHour | float | When it starts, from 0 to 24. 6.5 means half past six. |
DurationMinutes | float | How long it lasts, in minutes. |
Disabled | bool | true when the game’s data switches this entry off. |
Activity changes
ActivityChanged tells you when an NPC starts doing something different.
ActivityChangedArgs what you are told when an NPC's activity changes
ActivityChanged gives you one of these when an NPC starts a different activity. From and To are the old and new activities.
public sealed class ActivityChangedArgs
{
public int NpcId { get; }
public string From { get; }
public string To { get; }
}
| Property | Type | What it means |
|---|---|---|
NpcId | int | The NPC’s id. |
From | string | What it was doing. null when it had no activity. |
To | string | What it does now. null when its activity was cleared. |
NpcSchedules the activity change event
Subscribe to ActivityChanged to hear each time an NPC gets a different activity.
public static class NpcSchedules
{
public static event Action<ActivityChangedArgs> ActivityChanged;
}
| Event | Gives you | When it fires |
|---|---|---|
ActivityChanged | ActivityChangedArgs | An NPC has started a different activity. |
Things to know
ActivityChangedfires when the game gives an NPC a different activity, and also when the game clears it. When the activity is cleared,Toisnull. It does not fire when the game sets the same activity again. (it is raised from a postfix on the game’sNPCDataState.CurrentActivitysetter.)- The framework does not poll. It runs no timer. The game sets an idle NPC’s activity to nothing every frame, so the change is checked very often, but with no handler, or when nothing changed, it only reads one value and stops. It looks the NPC up only for a real change when you have a handler. (the game assigns
nullto an idle NPC’sCurrentActivityevery frame.)
Limits
- Loading a save does not fire it. A save load replaces the NPC’s whole state at once, so no single change happens. Changes the game makes right after the load do fire it.
- Do not start or stop NPC activities from inside your handler. Your handler runs inside the game’s own call, so wait until the game has finished, for example with a timer.
ActivityChangedalso fires while a save is loading, if an activity changes then. If you want to ignore those, checkSaveData.Busy, which istruewhile a save is loading or being written.
What you can’t do (yet)
- You can’t pause an NPC’s timetable. There is no single switch that works for every NPC. The game has a flag that is meant to pause an NPC that is away, but it does not stop an NPC that is in the scene, and the game saves it, so a flagged NPC would not appear when its scene loads and would seem to vanish. The game has another switch for an NPC in the scene, but it needs the live NPC and it stops the activity that is running. For a pause in the scene you are in, use
NpcMovement.SetActivityEnabled(see NPC movement). (the saved flag isNPCDataState.schedulerDisabled. The scheduler keeps starting activities for in-scene NPCs. The in-scene switch isNPCController.DisableActivity.) - You can’t send an NPC to a place from here. The game’s path search only works out a route and does not give the NPC a target. The call that gives an NPC a target only works in the loaded scene, and the timetable replaces that target as soon as the next activity starts. Use NPC movement instead. (
NPCPathfindingonly computes a path (SearchForPath). The NPC call isNPCController.SetTargetPosition.)
Details
Target: Obenseuer 0.5.08 (buildid 25969064), Unity 6000.0.77f1 Mono, BepInEx 6 Unity.Mono (be.788, the tested build) or BepInEx 5 (5.4.23 or later). Namespace: ObenseuerCore. Logs go to BepInEx\logs\obenseuer.moddingframework.npcschedules.log.
Everything not listed in this reference is internal and may change without notice. Items marked experimental may change too.
When you can call it
- Call everything from your mod’s normal code, not from a background thread you started. The game and Unity only work safely on one thread, the main thread (see the glossary). This includes the code that handles
ActivityChanged. - It is safe to call any of it at any time, even on the main menu or while a save is loading. If there is no NPC data for the id, you get
nullor an empty list. - Your event handlers run inside the game’s own call, one at a time. If one of your handlers throws an error, the framework logs it and still runs the other handlers. The event is only raised when at least one handler is listening.
What to check for and what comes back
- Nothing here throws. An id that is not known gives
nullor an empty list. - If the game itself fails while the framework reads, you get that plain default instead of an error. The first failure of each member is written to the log as an error. Later failures of that member are not logged again.
If something fails to set up
The framework changes some of the game’s methods when it starts, so it can tell your mod when an activity changes. If a game update breaks one of them, only that feature turns off, and the framework writes error lines and a warning to the log. The rest of the API keeps working.
- The change event depends on the feature group
obenseuer.moddingframework.npcschedules. This is the name you will see in the log. It has one game change in it: the setter for an NPC’s current activity. If the framework can’t make it in your game version, it turns the group off, writes error lines and a warning to its log file, andActivityChangednever fires.IsAvailableis thenfalse.CurrentActivityandTimetablekeep working. (the game method isNPCDataState.set_CurrentActivity.)