Docs/Your First NPC

Your First NPC

List your NPCs, connect to one, and stream a reply, in C++ and Blueprint.

ListNPCs finds an NPC, ConnectNPC opens a conversation. Everything after that is delegates.

List your NPCs

ListNPCs is async and Blueprint-callable, which is why it takes a callback delegate parameter rather than returning a value directly — even from C++, you bind to a UFUNCTION, not a lambda:

MyGameMode.cpp
FMistscaleListNPCsResult Callback;
Callback.BindUFunction(this, FName("OnNPCsLoaded"));
Mistscale->ListNPCs(Callback);

UFUNCTION()
void AMyGameMode::OnNPCsLoaded(bool bSuccess, const TArray<FMistscaleNPCSummary>& Npcs, const FMistscaleError& Error)
{
    if (!bSuccess || Npcs.Num() == 0)
    {
        UE_LOG(LogTemp, Warning, TEXT("No NPCs: %s"), *Error.Message);
        return;
    }
    // Npcs[0].Id is ready to pass to ConnectNPC
}

In Blueprint: drag off the Callback pin on List NPCsAdd Custom Event, and wire from there.

Connect and chat

MyGameMode.cpp
UMistscaleNPCConnection* Connection = Mistscale->ConnectNPC(Npcs[0].Id);

Connection->OnChatChunk.AddDynamic(this, &AMyGameMode::OnChatChunk);       // append streamed tokens
Connection->OnChatRevision.AddDynamic(this, &AMyGameMode::OnChatRevision); // grounding rewrote the reply — replace
Connection->OnChatMessage.AddDynamic(this, &AMyGameMode::OnChatMessage);   // the turn is settled
Connection->OnOpened.AddDynamic(this, &AMyGameMode::OnConnectionOpened);

UFUNCTION()
void AMyGameMode::OnConnectionOpened()
{
    Connection->SendChat(TEXT("Hello there!"));
}

If you don't want to render a live stream, just bind OnChatMessage and ignore OnChatChunk / OnChatRevision entirely — it always fires once per turn with the final text, whether or not you consumed the intermediate chunks. In Blueprint, bind delegates via Assign On Chat Chunk / Assign On Chat Message on the connection.

The connection surface

SendChat(Message, SenderId = "")
Sends a player message. SenderId overrides the connection's default player id for just this message.
SendVoiceChunk(Data, bEnd = false, SenderId = "")
Streams captured audio. Covered in Voice.
SetSpatialContext(Location, TimeOfDay = "", Weather = "")
Updates the NPC's sense of place. Covered in Spatial Context in Unreal.
GetEvolutionStatus() / GetQuotaStatus()
Requests a one-off OnEvolutionStatus / OnQuotaStatus delegate call with the NPC's current mood / usage snapshot.
Close()
Closes the connection and stops auto-reconnect.
GetState()
Connecting, Open, Closing, or Closed (EMistscaleConnectionState).

Delegates

OnOpened / OnClosed(Code, Reason, bExpected) / OnSdkError(Error)
Connection lifecycle. bExpected is true only for a Close() you called yourself.
OnChatChunk(ChatId, Delta)
One streamed token. Append it.
OnChatRevision(ChatId, Text)
The reply was corrected after generation. Replace your accumulated text with it.
OnChatMessage(ChatId, Text, bFinalizedByMetadata, Metadata)
The turn is done. Fires exactly once per turn regardless of whether you used OnChatChunk.
OnChatBlocked(ChatType, Reason, Limit, Used)
The NPC hit its usage quota and did not reply.
OnReconnecting(Attempt, DelayMs)
The plugin is about to retry an unexpected disconnect. Automatic; you don't need to call anything.
Who does the NPC think it's talking to?
Every connection has a player id — auto-generated if you don't set one in Configure, in which case two running game instances get two separate relationships with the same NPC. For a real game, pass your own stable player id via ConnectNPC(NpcId, PlayerId) so returning players are recognized across sessions and devices.
The subsystem holds your connections for you
UMistscaleSubsystem keeps a strong reference to every connection it creates for the lifetime of the game instance, so a returned UMistscaleNPCConnection* won't be garbage collected out from under you even if you don't keep your own reference.