NFO Format (Save Metadata Block)
Description: The savenfo (savenfo.res) file is a save’s load-menu metadata block (type NFO ). It sits loose in the save folder so the load screen can show a name, area, play time, and portraits for every slot without opening SAVEGAME.sav, an archive that grows with every module you visit. The engine builds it field by field at save time.
At a Glance
| Property | Value |
|---|---|
| Filename | savenfo.res |
| Magic Signature | NFO / V3.2 |
| Type | Save Metadata Block |
| Rust Reference | Handled by rakata-save (mid-refactor). |
Data Model Structure
A flat GFF of scalar fields, no lists (autosaves add one nested struct). The Save type column marks which write path emits each field: both, manual (manual saves and quicksaves), or autosave.
| Field | Type | Meaning | Save type |
|---|---|---|---|
SAVEGAMENAME | CExoString | Display name of the save. | manual |
AREANAME | CExoString | Localized display name of the current area. | both |
LASTMODULE | CExoString | Resref of the module the engine restores first. | both |
TIMEPLAYED | DWORD | Running play time, in seconds. | both |
CHEATUSED | BYTE | Cheat flag; mirrors the party table’s cheat state. | both |
GAMEPLAYHINT | BYTE | Loading-screen hint state. | both |
STORYHINT | BYTE | Loading-screen hint state. | both |
LIVE1 .. LIVE6 | CExoString | Downloadable-content slot names (six); empty on a vanilla PC install. | manual |
LIVECONTENT | BYTE | Bitmask of which of the six LIVE%d slots are installed; 0 when none. | manual |
PORTRAIT0 .. PORTRAITN | CResRef | One portrait resref per active party member. | both |
PCAUTOSAVE | BYTE | Always 1; its presence marks the file as an autosave. | autosave |
REBOOTAUTOSAVE | BYTE | Read by the load menu’s slot parser, but no write site exists anywhere in this build; see the note below. | none (dead on PC) |
SCREENSHOT | CExoString | Loading-screen resref (load_<module>) used as the slot preview, in place of a Screen.tga. | autosave |
AUTOSAVEPARAMS | Struct | Pending move-to-module state; see AUTOSAVEPARAMS Fields below. | autosave |
Engine Audits & Decompilation
(Documented from Ghidra decompilation of swkotor.exe (K1 GOG build). Manual-save writer: CServerExoAppInternal::StallEventSaveGame at 0x004b3110. The autosave variant is written by CServerExoAppInternal::DoPCAutosave at 0x004b8300.)
Note
savenfo quirks.
CHEATUSEDis not tracked here independently. The engine writes the same cheat flag the party table serializes, so the menu shows the right state without opening the party table.PORTRAIT0,PORTRAIT1, … is a numbered field series, not a GFF list: one field per active member, suffixed by index.- Downloadable (“live”) content always leaves a footprint. The manual/quicksave path writes all six
LIVE1-LIVE6name fields plus theLIVECONTENTbitmask unconditionally, whether or not content is installed. This is the Xbox Live download mechanism (LIVE%daliases resolving toRIMSXBOX\live%dRIMs). A vanilla PC install has nothing aliased in, so a real quicksave carries all sixLIVE%das empty strings andLIVECONTENT = 0(verified against a K1 GOG save). The Yavin Station DLC on PC is not delivered through this path.REBOOTAUTOSAVEis read unconditionally by the slot parser and folded into the same bit-field asPCAUTOSAVE, but no code path in this build ever writes it –DoPCAutosaveonly setsPCAUTOSAVE. The read path is live regardless: the save-list preview code checks this bit together withPCAUTOSAVEwhen it decides where a slot’s screenshot comes from, soREBOOTAUTOSAVEjust stays permanently0. See the Save Game Deep Dive for what the field likely inherits from.- This is the one file read straight from the slot for the load menu, and
LASTMODULEis what selects the first module to restore.
Important
Two write paths, two field sets.
savenfocomes fromStallEventSaveGame(manual saves and quicksaves) orDoPCAutosave(autosaves); the Save type column above marks which fields each produces.PCAUTOSAVEis the reliable discriminator. Autosaves also differ at the folder level (a loosepifo.ifo, noScreen.tga); the Save Game Deep Dive covers why.
AUTOSAVEPARAMS Fields
AUTOSAVEPARAMS snapshots the module transition that was in flight when the autosave fired, so the engine can resume it once the save is reloaded. Every field traces to a live piece of transition or world-clock state; none of it is autosave-invented data.
| Field | Type | Source |
|---|---|---|
LOADMUSIC | CExoString | The destination module’s loadscreens.2da row, MusicResRef column. Falls back to a load_<modulename> resref if that cell is empty and a matching .mp3 exists, then to the 2DA row literally named DEFAULT if neither resolves. Same fallback chain (different column) as the ordinary loading-screen background picker, CClientExoApp::SetLoadScreenByModuleName. |
STARTWAYPOINT | CExoString | The arrival waypoint tag passed to the StartNewModule script action by whatever door or trigger initiated the transition – where the player lands once the autosave resolves. |
MOVIE1 .. MOVIE6 | CExoString | Drained from the pending movie queue, filled by calls to AddMoveToModuleMovie from the same StartNewModule action, one movie per slot. Unfilled slots write as empty strings. There’s no queue-overflow handling to speak of: StartNewModule’s own script signature only accepts six movie arguments, so a seventh movie never reaches the autosave writer in the first place. |
TIME_YEAR | DWORD | The destination module’s start_year, fixed at module load rather than a running counter – this is why a real sample often reads 0. |
TIME_MONTH / TIME_DAY / TIME_HOUR | BYTE | The module’s live calendar fields (current_month/current_day/current_hour), read through the module’s own time accessor. |
TIME_MINUTE / TIME_SECOND / TIME_MILLISECOND | WORD | Not from the module’s calendar at all – derived by converting the world timer’s raw time-of-day tick count into minute/second/millisecond components. One snapshot, two different accessors: the module object supplies year/month/day/hour, the lower-level world timer supplies the rest. |
TIME_PAUSEDAY / TIME_PAUSETIME | DWORD | The paused day/time halves of the same world timer’s tick-pair snapshot – the identical live clock that seeds the module IFO’s Mod_PauseDay/Mod_PauseTime on an ordinary module-save, just captured at a mid-transition autosave instead. |
STATUSSUMMARY | Struct | See below. |
STATUSSUMMARY is a “since you last saw a loading screen” delta accumulator, not a snapshot of current totals:
| Field | Type | Behaviour |
|---|---|---|
CREDITS / XP / STEALTHXP | INT | Running totals added to by every credit/XP/stealth-XP gain since the popup last displayed. |
CREDITSNET | BYTE | Set when credits moved in both directions since the last display (gained and lost), distinguishing a net change from a one-way one. |
LIGHTSHIFT / DARKSHIFT | BYTE | Alignment-shift deltas, same accumulate-then-drain pattern. |
DISPLAYSPENDING / ITEMRECEIVED / ITEMLOST / JOURNAL | BYTE | Pending-event flags set by the corresponding gameplay hooks (item give/take, journal updates). |
SOUNDPENDING / LEVELUPSOUND / NEWQUESTSOUND / COMPLETESOUND | BYTE | Selects which stinger, if any, plays alongside the popup. |
SUPPRESSED | INT | Not a display toggle – a countdown. While positive, incoming gameplay events (credits, items, journal updates, and anything else listed above) are dropped rather than accumulated, and the counter ticks down by one per dropped event. Driven by the SuppressStatusSummaryEntry script command, presumably so a scripted sequence of item grants doesn’t leave a stale or inflated delta behind. |
The dispatcher (CGuiInGame::UpdateStatus) is the one place gameplay events get folded in; the drain point (CGuiInGame::ShowStatusSummary) zeroes the running totals and pending flags once the popup has been queued. Confirmed autosave-exclusive: StallEventSaveGame (the manual save/quicksave writer) never constructs this struct at all – there’s no in-flight transition for a manual save to preserve.
Implemented Linter Rules (Rakata-Lint)
None yet. Documented here ahead of any dedicated rakata-lint rules.