Saving & multiplayer

Intermediate Last updated Jul 9, 2026

On this page

Quest progress persists out of the box: in single player you don’t write a line of code, and the same model scales up to dedicated servers with one interface.

Autosave / autoload

Each player’s quest component loads stored progress when it starts and saves after changes (accept, objective credit, step advance, flags, completion). Saves are coalesced, so a burst of changes in one frame costs one write. A final save runs on quit, map travel and logout, and autoload restores progress across level changes automatically.

The policy is tunable in Project Settings → Plugins → Questwright Quests (details in the Settings reference):

Project Settings showing the Questwright Quests persistence options

Auto-persistence engages for components owned by a PlayerState (the shipped deployment); components placed on bare actors keep fully manual SaveProgress() / LoadProgress().

What is stored

Only the per-player progress records: state, current step, objective counters, flags, timestamps. Quest content lives in the compiled assets; an active dialogue scene is presentation and is never saved. Saved progress survives content updates: a record whose stored step no longer exists falls back to the quest’s first step (with a log warning), and records of quests unknown to the current build are kept untouched.

The three tiers

DeploymentWhat you do
Single playerNothing. One USaveGame slot (Questwright_Quests).
Listen-server co-opNothing. Each player gets an isolated slot keyed by their online id (Questwright_Quests_<id>).
Dedicated server / own backendImplement one C++ interface (below).

The backend seam is IQuestwrightSaveProvider:

class UMyDbSaveProvider : public UObject, public IQuestwrightSaveProvider
{
    // PlayerKey is the player's UniqueNetId string; use it as your database key.
    virtual bool SaveQuestRecords(const FString& PlayerKey,
                                  const TArray<FQuestwrightQuestRecord>& Records) override;
    virtual bool LoadQuestRecords(const FString& PlayerKey,
                                  TArray<FQuestwrightQuestRecord>& Out) override;
    // Optional non-blocking variant for REST/database fetches:
    // virtual void LoadQuestRecordsAsync(...) override;
};

Register it as SaveProviderClass in the settings page, or inject an instance with SetSaveProvider() on login. Everything runs server-side; clients receive loaded progress through normal replication.

Manual control

For a save-station game: set AutoSavePolicy to Manual and call SaveProgress() / LoadProgress() yourself. OnProgressLoaded fires after stored progress is applied.

Multiplayer model

Questwright works in standalone, listen server and dedicated server, with a strict server-authoritative split:

  • Progress is per player, on a component on the PlayerState, replicated owner-only, so players never see each other’s journal internals.
  • All mutations are server-authoritative. Client calls (accepting a quest from the UI, reporting an event) route through Server RPCs automatically; the API looks the same on either side.
  • Kill credit goes to the instigating player when your death code passes the killer; otherwise all players are credited (shared-world fallback).
  • Presentation never runs on the server. Cameras, faces, dialogue UI and look-at run only on the owning client; a dedicated server executes none of it.
  • Components are auto-provisioned on login, so late joiners get theirs automatically.