Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Save Game Deep Dive

KotOR stores a save as a folder, not a single file. This page documents what the folder contains, how the engine assembles it, and the serialization quirks that make save GFFs disagree with their static template counterparts. Evidence throughout is drawn from Ghidra decompilation of swkotor.exe (K1 GOG build), cross-checked against a vanilla save folder on disk. Findings are recorded as behaviour notes, not as transcribed engine code.

Overview

PropertyValue
On-disk unitA directory under the SAVES: alias, named NNNNNN - <name> (for example, 000231 - Game230); slots 000000 and 000001 are reserved, see below
Main archiveSAVEGAME.sav, an ERF with version tag MOD V1.0
Loose sidecarssavenfo.res, PARTYTABLE.res, GLOBALVARS.res (each a GFF), plus Screen.tga
Rust referencerakata-save (mid-refactor); see Save Games for the field tables

A save is the engine’s snapshot of the player’s whole session. That state spans every module you have visited, so the engine does not assemble it in one pass. It keeps a working directory while you play and packages that directory into the save folder when you save.

The staged working directory

The engine maintains a live working directory under the GAMEINPROGRESS: alias. As you play, two things accumulate there:

  • Per-module runtime state, written every time you leave a module.
  • Global session state (party table, globals), refreshed at save time.

When you save to a slot, the engine packages that working directory into SAVEGAME.sav and writes a few small metadata files loose alongside it. Loading reverses the process: unpack the archive back into a working directory and replay it.

Two consequences of this staging follow, and both are covered below: per-module state ends up as an archive nested inside SAVEGAME.sav, and the menu-facing metadata is kept loose, outside it.

The save folder layout

A vanilla save folder contains:

FileFormatRole
SAVEGAME.savERF (MOD V1.0)The bundle: every per-module archive plus the global session resources
savenfo.resGFF (NFO )Menu metadata: name, area, last module, play time, portraits
PARTYTABLE.resGFF (PT )Party roster, gold, XP, journal, available companions, pazaak, galaxy map
GLOBALVARS.resGFF (GVT )Campaign global variables (booleans, numbers, locations, strings)
Screen.tgaTGASave-slot preview thumbnail

These metadata files sit loose, and the load menu reads them straight from the slot: a name, area, play time, and thumbnail for every save. SAVEGAME.sav grows with every module you visit, so a late-game archive can run to megabytes, while savenfo.res stays a few hundred bytes. Reading only the tiny sidecar keeps the menu cheap no matter how large the archive has grown, and the thumbnail rides along beside it.

Two tag details matter to tooling: the PT party-table tag has two trailing spaces, and the ERF container’s tag is the full MOD V1.0. Every loose GFF is version V3.2, like every GFF the engine writes (see GFF for why the version never varies).

The filename casing is the engine’s own, and it is not uniform: SAVEGAME.sav, savenfo.res, PARTYTABLE.res, GLOBALVARS.res, and Screen.tga appear in exactly this mixed casing across every folder of a 104-save corpus and, independently, across saves written months apart on a second install. Read the names case-insensitively, but emit this exact casing when writing a save folder.

Per-module state: archives within the archive

A save is an archive of archives. Each module you visit is saved as its own ERF, and the engine bundles those per-module ERFs inside SAVEGAME.sav.

StoreCurrentModule writes a module’s ERF in two cases: when you save to a slot, and on every module transition, where it snapshots the module you are leaving before the next one loads. So a save holds the runtime state of every module you have visited, not just the current one. An IncludeModuleInSave gate decides which modules qualify: it reads modulesave.2da and excludes a module only when that table has a row for it set to 0. A missing row, or a modulesave.2da that fails to load, includes the module.

What a per-module ERF holds

Each per-module ERF carries the MOD V1.0 tag (created by CSWSModule::SaveModuleStart, finalized by CSWSModule::SaveModuleFinish) and holds three resources:

ResourceTypeWhat it holds
IFO (resref Module)module infoThe saved module clock, runtime id counters, and the party and limbo creature lists. See IFO for the save-only fields.
ARE 2012 (0x7dc)The area static. Skipped for Mod_IsNWMFile modules.
GIT 2023 (0x7e7)The dynamic object state: live creatures, doors, placeables, triggers, and the rest. Covered below.

Note

NWM is a “NeverWinter Module” (.nwm), a type the Odyssey engine inherited from BioWare’s Aurora engine (the one behind Neverwinter Nights). For an NWM module, the engine does not re-save the area static.

Faction state is global, not per-module

When the engine stores a module, it also rewrites the whole-session faction table to a single global REPUTE file (type FAC ; see FAC). That file ends up as the one REPUTE resource in SAVEGAME.sav, never duplicated per module.

The party roster

The active module’s IFO carries the party roster in Mod_PlayerList: one full creature snapshot per member, written by SavePlayers (structure in IFO).

Between modules the party can also live in a transient pifo (party-info) file: a GFF tagged IFO , written to the working directory by StorePlayerCharacters with the same Mod_PlayerList-of-creatures shape as a module roster. When it packs the party, StorePlayerCharacters stamps each player with the roster slot it was written to. LoadCharacterFromIFO normally reads a slot from the module’s Module IFO, but the sentinel index 0xffffffff switches it to pifo and reads back each player’s stamped slot. That is how the party crosses a module boundary: staged to pifo on the way out, restored from it on the way in (before the destination module’s roster exists), then rebuilt by CreateParty. The one call site that passes the sentinel is LoadCharacterStart; the module-roster path passes a real slot index instead.

The engine can also write the primary player character as a standalone Player file in the BIC character format (a SaveCreature snapshot behind a small header, written by SavePrimaryPlayerInfo; the format is Aurora’s character record, also used by character generation and transport). That path is gated behind a global flag and does not run for ordinary single-player saves, so do not rely on a Player.bic being present. There is no matching .bic reader: on load, the primary player is pulled from the module roster (LoadPrimaryPlayer takes the module’s primary-player index and hands off to LoadCharacterFinish), and any Player record is consumed through the same Mod_PlayerList creature-load path as a roster member, keyed by ObjectId. Don’t confuse this with the unrelated PC file covered below – different format, different trigger, different purpose; they only share the same underlying SaveCreature serializer.

The companion pool and the shared inventory

Mod_PlayerList only carries the members standing in the module. The rest of the recruited crew lives in the working directory as standalone UTC resources, one per companion, named AVAILNPC%d with the companion’s npc.2da row as the index (nine slots, AVAILNPC0-AVAILNPC8). Each is a full SaveCreature snapshot in its own GFF (typed UTC ), so every recruited companion lands inside SAVEGAME.sav whether or not they are in the active party. Slots never recruited have no file: an early-game save carries one or two AVAILNPC entries, a late-game save close to all nine.

The write path has two triggers. CSWPartyTable::AddNPC writes the snapshot the moment a companion is recruited (it also moves their inventory into the party stash, computes a joining-XP top-up, and adds them to the player’s faction). After that, SaveMember refreshes any slot that still has a live creature object: CSWPartyTable::Save loops all nine slots at save time, and party-member switches (SwitchPlayerCharacter) and the SaveNPCState script command hit the same function.

Reading back is gated by the party table. GetNPCObject refuses a slot unless its PT_NPC_AVAIL flag is set in PARTYTABLE.res, then instantiates the companion through the ordinary template loader (LoadFromTemplate) with the resref pointed at the save’s AVAILNPC%d resource, caching the live object id so the file is read at most once per session. The partytable flags are the index and the AVAILNPC files are the data: clearing a flag hides a companion whose snapshot still sits in the archive.

Two rejoin behaviours ride this path through SpawnNPC:

  • Placement. A member spawns at the position already on its creature snapshot. When that position resolves to a room of the current area, the engine adjusts it through ComputeSafeLocation (20-unit search radius) before adding the creature; a position that resolves to no room skips the adjustment and the member is added at the origin. In practice a member restored into the module it was saved in comes back where it stood. A member loaded back dead is resurrected on rejoin (GetNPCObject applies a resurrection effect when asked to).
  • XP catch-up. A rejoining member is topped up toward their share of the party XP pool: npc.2da’s PercentXP for their row, applied to PT_XP_POOL from the party table, with an auto-level-up when that client option is on. That is what the pool is for: benched companions do not earn XP live, they settle up when they rejoin.

The party’s shared item stash is a sibling resource: INVENTORY, a GFF typed INV holding a single ItemList of item snapshots. CSWPartyTable::UpdateInventory writes it right after the member loop at save time, and CreateParty reads it back when the party is rebuilt.

Gold and the party pool

Party wealth is a single number – PT_GOLD in PARTYTABLE.res – but every creature’s save block still carries its own Gold field, and on every load the two would fight if the engine weren’t careful. The routing that keeps them apart is deliberate, and it runs on both sides of the save cycle.

On the way out, SaveCreature briefly clears the creature’s in-party flag (the one SetInParty maintains) around its call into SaveStats. The gold accessors check that flag to decide whether “this creature’s gold” means its own private ledger or the shared pool, so clearing it makes the write capture the member’s frozen personal value instead of the live party total. On the way back in, ReadStatsFromGff uses the same flag as a read gate: a creature currently in the party skips its Gold field entirely, so each member’s stale personal snapshot can’t clobber PT_GOLD as their blocks load one after another. The pool is authoritative; the per-member snapshots are just along for the ride.

None of this applies to a creature that was never in the party. An ordinary NPC, a merchant, or a corpse has the flag clear throughout, and its Gold round-trips like any other field.

A benched companion’s AVAILNPCn snapshot falls into that same clear-flag case, but the reason is worth spelling out: GetNPCObject constructs a brand-new CSWSCreature (the in-party flag starts clear, same as any fresh object) and loads the companion’s AVAILNPCn GFF through LoadFromTemplate before anything downstream has a chance to set the flag. So a rejoining companion’s Gold reads back normally, not skipped the way an active member’s is. It doesn’t linger as personal currency once they’re active, though: AddMember immediately calls TransferInventory on the freshly-spawned creature, which folds that just-loaded Gold straight into PT_GOLD (and moves their items into the party stash in the same call) before the in-party flag is set to 1. First-time recruitment works the same way – AddNPC calls the identical TransferInventory, so whatever starting gold a companion’s .utc template carries gets folded into the pool the moment they’re recruited, not kept as a standing balance. AVAILNPCn’s Gold field isn’t a companion’s personal account; it’s a one-shot amount consumed the instant they go active. (Worth flagging, not confirmed as a bug: TransferInventory doesn’t appear to zero the source creature’s own gold member after folding it in, which would matter if the same live object were folded twice without an intervening reload – not traced further here.)

The stray PC file: a party-leader-swap artifact, not a save artifact

A save can carry a fourth flat resource that has nothing to do with the deliberate ones below it: a bare PC (UTC, SaveCreature-shaped) GFF, in the same raw format as an AVAILNPCn companion snapshot. CSWPartyTable::SwitchPlayerCharacter writes it to GAMEINPROGRESS:PC the moment module content invokes the SwitchPlayerCharacter script action to hand control away from the born player character, and reads it back through the same path when control returns to the PC. It’s a transient swap buffer for that one script action, not a save mechanism.

It only ends up inside SAVEGAME.sav as a side effect of how the archive gets built: StallEventSaveGame (manual saves and quicksaves) and DoPCAutosave both import the entire GAMEINPROGRESS: working directory into the ERF, unconditionally, sweeping up whatever loose files happen to be sitting there. Nothing clears GAMEINPROGRESS: between saves within a session – the whole directory is only wiped at session teardown (CServerExoAppInternal::StopServices, when you quit to the main menu or load a different game). So once a SwitchPlayerCharacter swap has fired anywhere in a play session, the PC file stays staged and gets archived into every save taken afterward: quicksave, manual save, or autosave alike, with no distinction between them.

That rules PC out as a save-type discriminator. Its presence tracks session history – has a leader-swap happened since the last full reset – not which writer produced the file, and it is entirely unrelated to the pifo roster-staging file or the gated Player BIC record covered above; the three just happen to reuse the same SaveCreature serializer for otherwise-unconnected purposes. Don’t read PC’s presence in a save as a sign of anything about how that save was made.

Building and reading SAVEGAME.sav

At save time, the engine imports the entire GAMEINPROGRESS: working directory into one ERF (StallEventSaveGame, via CERFFile::ImportFiles). The per-module ERFs already live in that working directory, so they land inside SAVEGAME.sav as nested resources, each keyed by its module resref under resource type 2057 (sav) — alongside the flat session resources that accumulate there too: the REPUTE faction table, the AVAILNPC companion snapshots, and the party INVENTORY (which is stored under the generic resource type 0, so look it up by name). That import is a blanket sweep of the whole working directory, not a curated list, so an incidental leftover like the PC file above can ride along too; treat the three named resources as the resources you can rely on, not as a closed inventory of everything a SAVEGAME.sav might contain.

Reading a module’s state back is a two-level walk:

  1. Open SAVEGAME.sav.
  2. Find the resource named after the module.
  3. Parse that resource as its own ERF, then read the GIT inside it.

The party table and global variables go into the same working directory at save time (CSWPartyTable::Save, CSWGlobalVariableTable::Save), which is why PARTYTABLE.res and GLOBALVARS.res also sit loose in the folder. Those loose copies are what the engine reads back for global session state.

The .rsv intermediate format: module state before packaging

The GAMEINPROGRESS working directory holds more than the flat session resources. Each visited module’s runtime state lives there as a standalone .rsv file before it is bundled into SAVEGAME.sav.

The .rsv extension maps to resource type RSV (0x0bc1), registered in the engine’s extension table (CExoBaseInternal::CreateResourceExtensionTable). It sits alongside the other extensions (.sav, .nwm, .mod, and dozens more) in a linear lookup table that GetResTypeFromExtension and GetResourceExtension walk.

Where they come from. When you leave a module, StoreCurrentModule snapshots the module state into GAMEINPROGRESS. The engine writes a per-module ERF through the same save pipeline (CSWSModule::SaveModuleStart / SaveModuleInProgress / SaveModuleFinish) that produces the nested ERFs inside SAVEGAME.sav. These ERFs land as <resref>.rsv files in the working directory, one per visited module, carrying the same three resources (IFO, ARE, GIT) as the packaged copies. A file dropped into GAMEINPROGRESS between transitions lands in the snapshot the same way.

How the engine uses them. At load time, CServerExoAppInternal::LoadModule checks for RSV before SAV, for any module that IncludeModuleInSave (the same modulesave.2da-driven gate the save side uses) says belongs in the save at all:

  1. If the module exists as an RSV resource, the engine resolves the filename through the GAMEINPROGRESS: alias and opens it as type RSV (0x0bc1).
  2. If RSV is not found, it falls back to SAV (type 0x0809, decimal 2057 – the same type the packaged archive copies use), the committed-archive copy.

This is a priority check, not a validation. The engine does not verify that a .rsv file came from its own save pipeline: it only checks whether one exists. A file dropped into GAMEINPROGRESS takes priority over the committed archive with no provenance check.

The RSV and SAV types are treated identically for the load-bar stall event: both trigger a type-3 (save-game) stall, where every other module type (MOD/RIM/NWM) triggers a type-1 (module) stall.

Important

is_nwm_file is not affected by whether a module loaded as RSV or SAV. It comes from one place only: the module’s own Mod_IsNWMFile IFO field, read the same way regardless of resource type (see the NWM note above). ARE loading is unconditional too – the area object always demands its ARE resource as the first step of loading, with no code path that skips it for any resource type. A hand-staged .rsv still needs a valid ARE to load successfully; it cannot get by on IFO and GIT alone.

During save size estimation. SetEstimatedSaveSize walks every file in GAMEINPROGRESS, maps each extension to its resource type, and estimates the eventual archive size. The padding isn’t type-keyed the way it might sound: any file that fails to open (regardless of its type) gets a flat 2.5 MB stand-in size rather than being skipped. Separately, once every file is summed, the estimate gets one more flat 2.5 MB added on top – but only when the type of the module currently being loaded is something other than RSV or SAV; loading from either of those is exempt from that particular top-up. An empty GAMEINPROGRESS directory skips both of these and uses a fixed 3.75 MB baseline instead. The whole total is then scaled up by roughly 11% before being stored as the estimate.

To packaging. When a save is committed, StallEventSaveGame (manual / quicksave) or DoPCAutosave calls CERFFile::ImportFiles to sweep the entire GAMEINPROGRESS directory into SAVEGAME.sav. The .rsv files land inside the archive as nested ERF resources, keyed by module resref under resource type 2057 (sav). A staged .rsv is indistinguishable from an engine-written one inside the archive. The .rsv extension is the working format; the archive uses the sav type. A tool that reads SAVEGAME.sav never sees the .rsv extension at all: it sees nested ERF resources, and their on-disk origin as .rsv files is an implementation detail of the staging directory.

Why this matters for tooling. If you are implementing a save reader that works from the GAMEINPROGRESS directory (for example, reading a session-in-progress without a committed save), the module files are .rsv, not .sav. The content is the same, but the filename extension and resource type differ. A tool that opens GAMEINPROGRESS directly needs to look for <module>.rsv rather than expecting a pre-built SAVEGAME.sav.

savenfo: the menu metadata block

savenfo.res is a small GFF (type NFO ) built field by field in StallEventSaveGame. Its field table lives on the savenfo page. The one behaviour worth recording here: CHEATUSED is not tracked independently. The engine writes the same cheat flag the party table serializes, so the load menu stays in sync without opening the party table.

The global sidecars: partytable and globalvars

The other two loose GFFs hold session-wide state that is not tied to any single module. Full field tables live on the partytable and globalvars pages; the engine-side notes worth recording here:

PARTYTABLE.res (type PT ) is written by CSWPartyTable::SaveTableInfo. The journal is folded into the same file by a helper (CSWPartyTable::SaveJournal) that SaveTableInfo invokes, and the entire journal block is omitted when the party journal is empty.

GLOBALVARS.res (type GVT ) is written by CSWGlobalVariableTable::WriteTable. It carries four global types, not two: boolean, number, location, and string, each a catalogue-name list paired with its own value block (VOID-packed for booleans, numbers, and locations; a list for strings). Tooling that models only numbers and booleans silently drops every location and string global. The globalvars page documents the byte-exact encoding of each value block (bit-packed booleans, one byte per number, a fixed 100-slot location array) and the per-type capacity limits.

Save types: manual, quicksave, and autosave

A save folder can come from either of two write paths, and they do not produce the same thing. A tool that parses a save should know which made it. The partytable/globalvars sidecars and the SAVEGAME.sav bundle are identical across types; the differences are confined to savenfo and two loose files:

Manual / quicksaveAutosave
WriterStallEventSaveGameDoPCAutosave
Fires whenyou save to a slot, or quicksaveyou cross into a new module (StartNewModule)
savenfo-only fieldsSAVEGAMENAME, LIVE1-LIVE6, LIVECONTENTPCAUTOSAVE (=1), SCREENSHOT, AUTOSAVEPARAMS
Slot thumbnailScreen.tga (captured frame)none; SCREENSHOT holds a load_<module> resref
pifo.ifoabsentpresent

PCAUTOSAVE is the reliable tell for an autosave: its presence means the file came from DoPCAutosave. AUTOSAVEPARAMS is a nested struct holding the pending move-to-module state; the savenfo page has the full field-by-field breakdown.

It does not separate a quicksave from a manual save, though. Both come from StallEventSaveGame and emit the same field set, so nothing inside the files tells them apart. The folder name does: every save folder is NNNNNN - <name>, and the first two slot numbers are reserved.

SlotFolderKind
000000000000 - QUICKSAVEQuicksave
000001000001 - AUTOSAVEAutosave
000002 and up000002 - Game1, …Manual saves, in creation order

There is no literal QUICKSAVE or AUTOSAVE directory; the words are the name half of the ordinary slot format. This is also why a save folder listing from a played-through install starts at 000002. To classify a slot: read the folder’s slot number, and corroborate with PCAUTOSAVE.

This isn’t just an observed pattern; the two reserved numbers are hardcoded literals, not something derived at runtime from folder scanning. The quicksave writer (CClientExoAppInternal::DoQuickSave) passes slot 0 alongside the literal string "QUICKSAVE". DoPCAutosave and a second, separate autosave trigger inside the server’s per-frame tick (CServerExoAppInternal::MainLoop) both pass slot 1 alongside "AUTOSAVE". The autosave loader, the save-menu filter, and the quickload finder each independently compare a folder’s parsed slot number against these same two literals.

The set of names that can ever reach this formatter is closed, established by walking every caller down to the shared write backend (CServerExoAppInternal::SaveGame) and the shared unpack/copy helpers:

NameSlot numberWriter
QUICKSAVE0 (literal)CClientExoAppInternal::DoQuickSave, via the generic SaveGame backend
AUTOSAVE1 (literal)DoPCAutosave (self-contained), and separately MainLoop’s periodic-autosave branch, which calls the same generic SaveGame backend directly
the player-entered save name2 and up (allocation mechanism below)The manual-save flow, through the same generic SaveGame backend

No fourth name reaches the formatter. Manual saves beginning at 000002 is confirmed, not just observed: nothing else in the traced call graph reserves a number below it.

Note

REBOOTAUTOSAVE never reaches the slot-name formatter at all – it isn’t a folder name. It’s a boolean byte field inside savenfo.res itself (alongside PCAUTOSAVE), read unconditionally by the load-menu’s slot parser, and its single cross-reference in the whole binary is that read. No code path writes it, quotes it as a save name, or feeds it to the formatter above; DoPCAutosave only ever sets PCAUTOSAVE. The save-list preview code checks this bit together with PCAUTOSAVE when it decides where to pull a slot’s screenshot from, so the read path is live. With no PC producer, though, the field is permanently 0 in every save this build creates. The name suggests a hard-reset or dashboard-return autosave, most likely inherited console-SKU logic tolerantly parsed here for compatibility, the same shape as the already-documented LIVE%d Xbox content mounts. Two other REBOOT/AUTOSAVE-adjacent strings turned up during this trace and were ruled out as unrelated: CB_AUTOSAVE is an options-screen checkbox control id, and AutoSave/AutoSaveOnEnter are module/area property names – neither is a save-folder name.

Allocating a new manual-save slot number happens in the Save menu’s list builder (CSWGuiSaveLoad::PopulateGameList), which filters out any folder parsed as slot 0 or 1 before manual saves are ever considered (the Load menu skips this filter, so quicksave/autosave still show up there). Over what’s left, the engine tracks both the highest existing manual-save number and the lowest unused one starting from 2. The normal case appends after the highest number found; if that would push past 999, the commit step (CSWGuiSaveLoad::WriteGame) falls back to the first gap instead, and if even the gap search comes up empty – all of 2-999 occupied with no room – the save is rejected outright. 999 manual saves is a hard ceiling, not a soft one.

Both loose-file differences trace to autosaves firing mid-transition (StartNewModule), while a loading screen is up:

  • pifo.ifo exists because at a transition the party is staged in the transient pifo party-info file rather than a module roster (the same file the 0xffffffff load path reads, see The party roster); DoPCAutosave copies it into the folder. It is a GFF tagged IFO holding a Mod_PlayerList of the party.
  • No Screen.tga because there is no gameplay frame to capture mid-loading-screen, so the autosave records the loading-screen resref in SCREENSHOT instead. (The slot thumbnail is independent of the EnableScreenShot ini option, which governs only the manual F12 screenshot.)

Treat a stray pifo.ifo, or a missing Screen.tga, as an expected autosave artifact rather than malformed data.

The GIT: dynamic object state

A module’s live objects (creatures, doors, placeables, triggers, items, and so on) are serialized into a GIT GFF inside the module ERF (CSWSArea::SaveGIT). The engine walks the area’s object array, buckets each object by runtime type, and emits one list per type:

GIT list labelObjectNotes
Creature Listcreaturesplayer characters are split out into a separate player list, not this one
Listitem instancesitems in the area use the bare label List
Door Listdoors
TriggerListtriggers
Encounter Listencounters
WaypointListwaypoints
SoundListsounds
Placeable Listplaceablescorpses are excluded
StoreListstores
AreaEffectListarea-of-effect objects

The list labels are gleefully inconsistent: some are spaced (Creature List, Door List, Encounter List, Placeable List), some are jammed together (TriggerList, WaypointList, SoundList, StoreList, AreaEffectList), and item instances get the bare word List. There is no rule to derive them; they are simply the literal strings the engine hardcodes.

Alongside the object lists, the GIT struct carries area-level state: CurrentWeather, WeatherStarted, TransPending, TransPendNextID, TransPendCurrID (all BYTE), plus script variable tables.

Templates versus snapshots: the UseTemplates flag

The most important thing to know about a save GIT: the same GIT schema is read two completely different ways, chosen by a single UseTemplates BYTE in the GIT’s top-level struct. The area loader (CSWSArea::LoadGIT) reads UseTemplates once and hands it to every per-type loader.

AspectUseTemplates = 1 (static .git)UseTemplates = 0 (savegame GIT)
Object elementsparse placementfull self-contained snapshot
TemplateResRefpresentabsent (not read)
Blueprint loadyes, via the object’s LoadFromTemplate (UTC/UTD/UTP/UTT/…), then instance fields overlaidnone; the engine reads every field directly (CSWSCreature::LoadCreature, CSWSDoor::LoadDoor, …)
Where the data livesmostly in the blueprintentirely in the GIT element
A field missing from the elementcomes from the blueprintcomes from the engine’s hardcoded default

The consequence for any field that is absent from a savegame element: it falls back to the engine’s hardcoded default, not to the blueprint. The per-object loaders read each field with a default argument (for example a missing trigger TrapType defaults to the value already on the object, and most BYTE fields default to zero). So a savegame instance carries everything the saver wrote, and nothing more: anything the saver left out comes from an engine default, never from the template.

Important

When reading a savegame, do not reach for the blueprint. A UseTemplates = 0 object is the whole truth; any field it leaves out comes from the engine’s hardcoded default, never the .utc / .utd / … template. Synthesizing a template lookup for a savegame instance invents data the engine never used.

The two GIT roles now line up cleanly. A module’s static layout is the sparse, template-relative form; a savegame is the already-flattened form with runtime overrides baked in. To read “the object as it exists in this save,” a tool reads the savegame GIT directly under UseTemplates = 0; only a module’s own .git needs the blueprint flatten. Evidence: CSWSArea::LoadGIT, CSWSArea::LoadCreatures, CSWSCreature::LoadCreature, and CSWSCreature::LoadFromTemplate.

Note

Area-level state (weather, the script variable tables) is gated separately, by LoadGIT’s own caller flag, not by UseTemplates. It is restored only on a full area load.

Object fields

Save objects use the ordinary GIT object schema, so the full per-object field map, the per-type position/orientation naming, and the geometry conventions live in the GIT format spec and the per-object specs (for example, UTT for trigger geometry). This page doesn’t repeat those tables. It records only the points the save path adds or makes clearer:

Save-path behaviourWhat the engine does
Field names vary by object typeNo generic position or orientation field exists. Positions come in three spellings: X/Y/Z, XPosition/YPosition/ZPosition, and PositionX/PositionY/PositionZ (area-of-effects only). Orientations come in four: the Bearing scalar, the XOrientation/… vector, the OrientationX/… vector, and a single-float Orientation on encounter spawn points. Sounds and encounters store none at all.
Bearing is two different thingsA door stores its Bearing verbatim. A placeable derives it from its orientation yaw at save time, so the stored value is lossy. Confirmed in CSWSDoor::SaveDoor and CSWSPlaceable::SavePlaceable.
Vector orientation is stored in fullCreatures, triggers, waypoints, and stores keep the whole orientation vector; nothing reduces it to a yaw at save time. On load the vector goes to SetOrientation, normalized first if it is not unit length (confirmed for triggers). Only the trigger geometry re-bake consumes the yaw alone.
Trigger geometry is position-relative and orientation-coupledTrigger vertices are offsets from the trigger’s position, not absolute points. Supplying an orientation on load re-rotates the geometry by the yaw delta.
Creature stat totals are recomputed, not restoredMaxHitPoints, ArmorClass, and the saving-throw totals in a creature block are write-only snapshots; the engine rebuilds them on load from inputs that round-trip through other fields. Details on the UTC page.
DetectMode never survives a reloadWritten faithfully, read only to skip past, then reset to 1 by construction. One of a small family of creature round-trip quirks catalogued on the UTC page.

Loading a save: the unpack flow

Loading mirrors the staged working-directory model in reverse, and the engine never runs a session from the slot itself. CSWGuiSaveLoad::UnpackGame drives the sequence:

  1. The load menu reads the loose savenfo.res straight from each slot for its name, area, play time, and thumbnail; no archive is opened. When you pick a slot, its LASTMODULE names the first module to restore.
  2. CopyGameToFutureGame unpacks the chosen slot, the SAVEGAME.sav ERF plus the loose sidecars, into the FUTUREGAME: staging area.
  3. The engine clears GAMEINPROGRESS: and renames FUTUREGAME: onto it. The live session runs from this unpacked copy.
  4. CSWSModule::LoadModule replays the module against the working directory, running LoadModuleStart / LoadModuleInProgress / LoadModuleFinish (the inverse of StoreCurrentModule). LoadModuleStart reads the per-module IFO including its save-only fields, loads the global REPUTE faction table (LoadFactionsFromSaveGame / LoadReputationsFromSaveGame), and reads the GIT with UseTemplates = 0, so every object comes from its full snapshot.
  5. The party table and global variables come back from their loose sidecars.

Note

A successful load never modifies the slot; only the working directory changes. If the unpack fails partway, the engine drops a CORRUPT marker file into the slot and abandons the load.

The CORRUPT.res marker

The marker is a real file, CORRUPT.res, written straight into the slot folder with the literal ASCII text "CORRUPT" as its entire content – a sentinel, not a structured GFF. UnpackGame (and the equivalent quicksave path, CGuiInGame::UnpackQuickSaveGame) writes it whenever the archive-copy step (CopyGameToFutureGame / CopyQuickSaveGameToFutureGame) fails.

Tracing that copy step down to the raw file I/O turned up something worth flagging: none of the ordinary return-code paths in the ERF-reading chain (CERFFile::Read, ReadHeaderVariance, ExportFilesFromERF, down to the raw file read/write calls) actually signal failure for a missing, truncated, or garbled file – they tolerate short reads with a retry-and-log, and CopyGameToFutureGame itself has only one return path, an unconditional success. So the marker-writing branch isn’t reachable through a checked validation at all. It can only fire if a genuine exception unwinds out of the copy, and the functions in this chain do install real C++ exception handling. The most plausible trigger, going by what the code does with the data, is a corrupted archive header’s entry/language counts driving an allocation or a read past what’s actually there – but there’s no explicit corruption check anywhere in this path to point to as “the” validation. Treat that as the likely mechanism, not a confirmed one.

On the read side, CSWGuiSaveLoadEntry::LoadData (the per-slot load-menu populator) checks for the marker by trying to open it; if present, the entry skips reading savenfo.res entirely (area name, last module, and play time stay blank) and unmounts the slot’s resource directory. A corrupted slot still shows up in the load-menu list, though, with the same click and delete handlers as any other entry – the marker only blanks the preview (area name, screenshot, and party portraits) and disables the Delete button when that entry is hovered or selected. Nothing stops the player from trying to load it anyway; if the underlying fault reproduces, the attempt silently abandons again with no error dialog.

Nothing clears the marker by name – there’s no code that specifically deletes CORRUPT.res. It does disappear as a side effect of a normal save, though: writing to an existing slot wipes every recognized resource file in that folder before laying down the new SAVEGAME.sav/savenfo.res/screenshot, and .res is a recognized extension, so CORRUPT.res gets swept up in that generic cleanup along with everything else. A subsequent successful save to a marked slot clears it, but only incidentally.

The read side is symmetric with the documented write side. The load-only behaviours are noted where they occur: the reputation default-baseline restore (FAC), the FactionGlobal default, and the trigger geometry re-bake (UTT).

What this means for tooling

  • Treat a save as a folder: one MOD V1.0 ERF plus three loose GFF sidecars and a thumbnail.
  • Read a module’s runtime objects with a two-level archive walk: open SAVEGAME.sav, find the module’s nested ERF, then read the GIT inside it.
  • Check UseTemplates first. A savegame GIT (UseTemplates = 0) is self-contained, so read objects directly. A module’s static .git (UseTemplates = 1) is template-relative, so resolve each TemplateResRef against the module’s blueprints and overlay the instance fields.
  • Resolve a savegame object’s missing fields to engine defaults, never to a blueprint. A template lookup supplies values the engine does not use.
  • Branch position and orientation field access on the object type; no single field name covers every type.
  • Preserve the raw GFF tree to round-trip placeable bearing and trigger geometry byte-exactly; on disk those values are derived, lossy, or position-relative.

Miscellaneous engine notes

Several engine behaviours touch more than one part of a save. Each is covered in full on the linked pages or in the sections above; this section summarizes them.

  • Global value blocks use a fixed byte layout: bit-packed booleans (most-significant bit first), one byte per number, and a fixed 100-slot location array. Each location slot is a 24-byte CScriptLocation: a position vector (X, Y, Z) followed by an orientation vector (X, Y, Z), six little-endian floats with no area reference.
  • The pifo party-info file is a working GFF tagged IFO . LoadCharacterFromIFO reads it when the requested member index is 0xffffffff.
  • A Player.bic has no dedicated reader. The engine loads it through the ordinary Mod_PlayerList creature-load path, keyed by ObjectId.
  • IncludeModuleInSave decides which visited modules are written into the save, using modulesave.2da.
  • A saved orientation is written as a full (X, Y, Z) vector, never reduced to a yaw.

Key Ghidra addresses

For anyone continuing this archaeology, the foundation set of function addresses in swkotor.exe (K1 GOG build):

FunctionAddress
CServerExoAppInternal::StallEventSaveGame0x004b3110
CServerExoAppInternal::SaveGame0x004b58a0
CServerExoAppInternal::DoPCAutosave0x004b8300
CServerExoAppInternal::StartNewModule0x004ba920
CServerExoAppInternal::StoreCurrentModule0x004b2e70
IncludeModuleInSave0x004b20e0
CSWSModule::SaveModuleStart0x004c8960
CSWSModule::SaveModuleInProgress0x004c3b10
CSWSModule::SaveModuleFinish0x004ca680
CSWSModule::SavePrimaryPlayerInfo0x004c3c70
CSWSModule::SavePlayers0x004c7870
CSWPartyTable::Save0x005665c0
CSWPartyTable::SaveTableInfo0x005648c0
CSWPartyTable::SaveJournal0x00563d90
CSWPartyTable::AddNPC0x00564300
CSWPartyTable::SaveMember0x00563e80
CSWPartyTable::UpdateInventory0x00564030
CSWPartyTable::GetNPCObject0x00564700
CSWPartyTable::CreateParty0x00565760
CSWPartyTable::SpawnNPC0x00565130
CSWGlobalVariableTable::Save0x0052ad10
CSWGlobalVariableTable::WriteTable0x005299b0
CSWSArea::SaveGIT0x0050ba00
CSWSArea::SaveCreatures0x00507680
CSWSArea::SaveDoors0x00507810
CSWSArea::SaveTriggers0x005078d0
CSWSArea::SavePlaceables0x00507bd0
CSWSDoor::SaveDoor0x00588ad0
CSWSPlaceable::SavePlaceable0x00586a70
CSWSTrigger::SaveTrigger0x0058e660
CSWSCreature::SaveCreature0x00500610
CSWSCreatureStats::SaveStats0x005b1b90
CSWSWaypoint::SaveWaypoint0x005c8230
CSWSStore::SaveStore0x005c6cd0
CSWSSoundObject::Save0x005c86d0
CSWSEncounter::SaveEncounter0x00591350
CSWSAreaOfEffectObject::SaveEffect0x00594d80
CResGFF::CreateGFFFile0x00411260
CSWSArea::LoadGIT0x0050dd80
CSWSArea::LoadCreatures0x00504a70
CSWSCreature::LoadCreature0x00500350
CSWSCreature::LoadFromTemplate0x005026d0
CSWSTrigger::LoadTrigger0x0058da80
LoadTriggers0x0050a350
LoadTriggerGeometry0x0058d060
CSWGuiSaveLoad::UnpackGame0x006caaf0
CopyGameToFutureGame0x006c9a90
CSWSModule::LoadModule0x004b95b0
CSWSModule::LoadModuleStart0x004c9050
CServerExoAppInternal::LoadPrimaryPlayer0x004b5f50
CServerExoAppInternal::LoadCharacterStart0x004b7470
CServerExoAppInternal::LoadCharacterFinish0x004b5c50
CServerExoAppInternal::StorePlayerCharacters0x004b2ba0
CSWSPlayer::LoadCharacterFromIFO0x00561e30
CSWGlobalVariableTable::ReadTableWithCatalogue0x0052a280
CSWGlobalVariableTable::GetValueLocation0x00529350
CSWSObject::GetScriptLocation0x004cb7b0
CFactionManager::LoadFactionsFromSaveGame0x0052b5c0
CFactionManager::LoadReputationsFromSaveGame0x0052bbe0

Located during the per-object savegame-defaults audit (creature, item, door, placeable, trigger, waypoint, store, sound, encounter, area-of-effect, and module/area/GIT top-level fields), covering the loaders, writers, and constructors behind the defaults recorded on each type’s page:

FunctionAddress
CSWSCreatureStats::ReadStatsFromGff0x005afce0
CSWSCreatureStats::ReadSpellsFromGff0x005aeb30
CSWSCreatureStats::SaveClassInfo0x005aec90
CSWSCreatureStats::CSWSCreatureStats (constructor)0x005aca80
CCombatInformation::LoadData0x00552350
CCombatInformation::SaveData0x00550f30
CSWSCombatRound::LoadCombatRound0x004d5120
CSWSPlayer::LoadCreatureData0x00560e60
CSWSMessage::SendServerToPlayerUpdateCharResponse0x00570c60
CSWSCreature::ReadScriptsFromGff0x004ebf20
CSWSCreature::LoadFollowInfo0x004fb180
CSWSCreaturePartyFollowInfo::Load0x004eb020
CSWSCreaturePartyFollowInfo::Save0x004eaf70
CSWSCreaturePartyFollowInfo::CSWSCreaturePartyFollowInfo (constructor)0x004f79e0
CSWSObject::LoadListenData0x004d0480
CSWSObject::SaveListenData0x004cca50
CSWSObject::LoadObjectState0x004d1cf0
CSWSObject::SaveObjectState0x004cec50
CSWSObject::LoadEffectList0x004d1be0
CSWSObject::SaveEffectList0x004cc9d0
CGameEffect::LoadGameEffect0x005043a0
CSWSObject::LoadActionQueue0x004cecb0
CSWSObject::SaveActionQueue0x004cc7e0
CSWSObject::CSWSObject (base constructor)0x004cfcb0
CSWSScriptVarTable::LoadVarTable0x0059aa80
CSWSScriptVarTable::SaveVarTable0x0059adb0
CSWVarTable::LoadVarTable0x0059b0f0
CSWVarTable::SaveVarTable0x0059b250
CSWSCreature::ReadItemsFromGff0x004ffda0
CSWSCreature::CSWSCreature (constructor)0x004f7a10
CSWSCreature::SetDetectMode0x0050ee30
CSWSCreature::SetStealthMode0x0050ee50
CSWSModule::LoadLimboCreatures0x004c8c70
CSWSModule::SaveLimboCreatures0x004c5bb0
CSWSModule::LoadModuleInProgress0x004c5720
CSWSArea::LoadArea0x0050e190
CItemRepository::GetItemRepository0x004ef770
CSWSItem::LoadItem0x00560970
CSWSItem::LoadFromTemplate0x005608b0
CSWSItem::LoadDataFromGff0x0055fcd0
CSWSItem::CSWSItem (constructor)0x005530a0
CSWItem::CSWItem (base constructor)0x005b4660
CSWSItem::SetPossessor0x00553210
CSWSItem::SaveItem0x0055ccd0
CSWSItem::SaveItemProperties0x00555790
CSWSItem::SaveContainerItems0x0055cfa0
CSWSItem::ReadContainerItemsFromGff0x0055f0f0
CSWSArea::LoadItems0x00504de0
CSWSArea::SaveItems0x00507750
CSWSDoor::LoadDoor0x0058a1f0
CSWSDoor::LoadFromTemplate0x0058b3d0
CSWSDoor::LoadDoorExternal0x0058c5f0
CSWSDoor::CSWSDoor (constructor)0x00589ee0
CSWSDoor::PostProcess0x00589d40
CSWSArea::LoadDoors0x0050a0e0
CSWSPlaceable::LoadPlaceable0x00585670
CSWSPlaceable::LoadFromTemplate0x00587a70
CSWSPlaceable::CSWSPlaceable (constructor)0x005877e0
CSWSPlaceable::LoadBodyBag0x005864b0
CSWSPlaceable::SpawnBodyBag0x004ce220
CSWSPlaceable::AcquireItem0x00584b10
CSWSPlaceable::PostProcess0x00584870
CSWSArea::LoadPlaceables0x0050a7b0
ExecuteCommandCreateObject0x0052f820
CSWSTrigger::CSWSTrigger (constructor)0x0058eae0
CSWSTrigger::LoadFromTemplate0x0058ed70
CSWSTrigger::AddToArea0x0058f030
CSWSWaypoint::LoadWaypoint0x005c7f30
CSWSWaypoint::CSWSWaypoint (constructor)0x005c7e70
CSWSArea::LoadWaypoints0x00505360
CSWSStore::LoadStore0x005c7180
CSWSStore::LoadFromTemplate0x005c7760
CSWSStore::CSWSStore (constructor)0x005c6ab0
CSWSStore::AddItemToInventory0x005c70c0
CSWSArea::LoadStores0x005057a0
CSWSSoundObject::Load0x005c9040
CSWSSoundObject::LoadFromTemplate0x005c94e0
CSWSSoundObject::CSWSSoundObject (constructor)0x005c8f30
CSWSArea::LoadSounds0x00505560
CSWSEncounter::ReadEncounterFromGff0x00592430
CSWSEncounter::ReadEncounterScriptsFromGff0x00590820
CSWSEncounter::LoadEncounter0x00593830
CSWSEncounter::LoadFromTemplate0x00593a90
CSWSEncounter::LoadEncounterGeometry0x00590580
CSWSEncounter::LoadEncounterSpawnPoints0x00590410
CSWSEncounter::CSWSEncounter (constructor)0x00593c70
CSWSArea::LoadEncounters0x00505060
CSWSAreaOfEffectObject::LoadEffect0x00594b00
CSWSAreaOfEffectObject::CSWSAreaOfEffectObject (constructor)0x00594480
CSWSArea::LoadAreaEffects0x00505af0
CSWSArea::LoadProperties0x00507490
CSWSArea::SaveProperties0x00506090
CSWSArea::LoadMaps0x00505da0
CSWSArea::SaveMaps0x005061d0
CSWSArea::LoadPlaceableCameras0x00505eb0
CSWSArea::SavePlaceableCameras0x005062a0
CSWSModule::SaveModuleIFOStart0x004c7050
CSWSModule::SaveModuleIFOFinish0x004c8b90
CSWSModule::SaveStatic0x004c5980
CSWSAmbientSound::CSWSAmbientSound (constructor located; Load/Save not individually decompiled)0x005c95a0
MainLoop (two call sites feed LoadModuleStart/limbo-creature handling)0x004babb0, 0x004ae860

Located while resolving four open questions left by the defaults audit above (creature stat recomputation on load, the Gold read gate, Mod_StartMovie, and area-of-effect script re-derivation):

FunctionAddress
CSWSCreatureStats::GetFortSavingThrow0x005ab810
CSWSCreatureStats::GetWillSavingThrow0x005ab880
CSWSCreatureStats::GetReflexSavingThrow0x005ab8f0
CSWSCreatureStats::GetBaseFortSavingThrow0x005aa1b0
CSWSCreatureStats::GetBaseWillSavingThrow0x005aa2f0
CSWSCreatureStats::GetBaseReflexSavingThrow0x005aa430
CSWSCreature::GetArmorClass0x004ed1d0
CSWCCreatureStats::GetArmorClass (client-side display cache, not the save source of truth)0x00647720
CSWSItem::ComputeArmorClass (an item’s own base AC contribution, unrelated to the creature-level getter above)0x00553cc0
CSWSObject::GetMaxHitPoints0x004d01a0
CSWSCreature::GetMaxHitPoints0x004ed310
CSWCCreatureStats::GetMaxHitPoints (client-side display cache)0x00647a80
SaveCharGenCreature (character-generation/BIC export path; also writes MClassLevUpIn and PregameCurrent, with no reader for either)0x006123e0
CSWSCreature::GetGold0x004edd60
CSWSCreature::SetGold0x004edda2
CSWSCreature::AddGold0x004f3dc8
CSWSCreature::RemoveGold0x004f3eea
CSWSCreature::TransferGold0x004fd769
CSWSCreature::SetInParty0x004fdb2d
CSWSCreature::CSWSCreature (a second constructor address located in this pass; the address recorded earlier in this table, 0x004f7a10, was found in an earlier session – likely a different overload, not reconciled)0x004f7b47
CSWSPlayer::LoadLocalCharacter0x00561d70
ExecuteCommandAddPartyMember0x0052de70
ExecuteCommandRemovePartyMember0x00541c00
SwitchPlayerCharacter0x005667c0
TransferInventory0x005641e0
CSWSAreaOfEffectObject::LoadAreaEffect (singular; the vfx_persistent.2da-driven definition lookup, wired only to fresh spell-cast creation, never to a save load)0x005947b0
CSWSEffectListHandler::OnApplyAreaOfEffect0x004dade0
ApplyEffect0x0050c6b0
CSWSAreaOfEffectObject::AIUpdate (heartbeat tick)0x00595d10
CSWSAreaOfEffectObject::EventHandler (enter/exit collision events)0x005964e0
CVirtualMachineInternal::RunScript0x005d45d0
CVirtualMachine::RunScript (thin forwarder)0x005d0fc0
CSWSAreaOfEffectObject::GetEffectSpellId / SetEffectSpellId0x005945d0 / 0x005945e0

Located while auditing save-slot numbering (QUICKSAVE/AUTOSAVE/REBOOTAUTOSAVE):

FunctionAddress
CClientExoAppInternal::DoQuickSave0x005f4b50
CSWGuiSaveLoadEntry::LoadData0x006c8e50
CSWGuiSaveLoad::LoadPCAutoSave0x006ca250
CSWGuiMainMenu::OnPanelAdded (disk-space probe reusing the reserved AUTOSAVE name)0x0067b6c0
CSWGuiSaveLoad::PopulateGameList0x006cc160
CSWGuiSaveLoad::HandleSaveButton0x006cbb60
CSWGuiSaveLoad::PromptForSaveName0x006cb820
CSWGuiSaveLoad::WriteGame0x006c8790
CSWGuiSaveLoad::ShowGame0x006c89d0
CSWGuiSaveLoadEntry::SetXboxTitle0x006c9780
CGuiInGame::DoQuickLoad0x00633c50
CSWGuiSaveLoadEntry::CSWGuiSaveLoadEntry (constructor)0x006cb940

Located while enumerating AUTOSAVEPARAMS:

FunctionAddress
KOTOR_AUTOSAVE_PARAMS::SaveToGFF0x004b28e0
CStatusSummary::SaveToGFF0x004b26c0
CGuiInGame::GetStatusSummary / SetStatusSummary0x0062f0a0 / 0x0062f040
CGuiInGame::SuppressStatusSummary0x0062f0c0
CGuiInGame::GetPendingStatusSummary0x0062ef70
CGuiInGame::ShowStatusSummary0x0062ef90
CGuiInGame::UpdateStatus0x0062eeb0
CSWGuiStatusSummary::AddAlignmentShift0x00624a70
CSWGuiStatusSummary::AddCredits0x00624ab0
CSWGuiStatusSummary::AddXp0x0062b580
CSWGuiStatusSummary::AddStealthXp0x0062b5a0
CSWVirtualMachineCommands::ExecuteCommandSuppressStatusSummaryEntry0x00547e50
CSWVirtualMachineCommands::ExecuteCommandStartNewModule0x00544390
CClientExoApp::GetMoveToModuleMovies0x005edb60
CClientExoApp::AddMoveToModuleMovie0x005edb50
CClientExoApp::RemoveMoveToModuleMovies0x005ee380
CServerExoApp::GetMoveToModuleStartWaypoint / SetMoveToModuleStartWaypoint0x004aed40 / 0x004aed30
CServerExoApp::SetMoveToModulePending0x004aecc0
CServerExoApp::SetMoveToModuleString0x004aecd0
CClientExoApp::SetLoadScreenByModuleName0x005edcf0
CClientExoApp::GetLoadMusicByModuleName (thunk) / CClientExoAppInternal::GetLoadMusicByModuleName (implementation)0x005edd00 / 0x005f3650
CWorldTimer::GetWorldTime0x004ade40
CWorldTimer::ConvertFromTimeOfDay0x004add90
CSWSModule::GetTime0x004c4100

Located while tracing the CORRUPT.res marker:

FunctionAddress
CGuiInGame::UnpackQuickSaveGame0x006323a0
CopyQuickSaveGameToFutureGame (quicksave counterpart to CopyGameToFutureGame; not fully decompiled, assumed the same shape by symmetry)0x0062fbe0
CERFFile::Read0x005dce50
CERFFile::ReadHeaderVariance0x005dd3c0
CERFFile::ExportFilesFromERF0x005dd710
CERFRes::CopyToFile0x005dd170
CExoFile::FileOpened / Read / Write0x005e6a10 / 0x005e6960 / 0x005e69a0
CExoFileInternal::Read / Write0x005eba40 / 0x005ebc60
CSWGuiSaveLoad::VerifyLoadGame0x006cc0e0
CSWGuiSaveLoad::LoadGame0x006cb0e0
CExoResMan::CleanDirectory0x00409460
CExoResMan::WipeDirectory0x00408e90
CExoAliasListInternal::ResolveFileName0x005eb6b0
CExoBaseInternal::GetResourceExtension0x005e7a00

Located while closing the reserved-name class (proving no fourth slot name reaches the formatter):

FunctionAddress
CSWGuiSaveLoadEntry::GetGameDirectory0x006c8250
CServerExoApp::SaveGame (thin wrapper over CServerExoAppInternal::SaveGame, dispatched from network message handlers)0x004ae6e0
HandlePlayerToServerModuleMessage / HandleServerAdminToServerMessage0x00524800 / 0x00528380
CSWGuiSaveLoad::DeleteGame0x006caa90

Located while tracing the stray PC resource to SwitchPlayerCharacter (the writer/reader itself, CSWPartyTable::SwitchPlayerCharacter at 0x005667c0, was already in this table from an earlier pass):

FunctionAddress
ExecuteCommandSwitchPlayerCharacter (nwscript action dispatch, sole caller of SwitchPlayerCharacter)0x00544910
CSWPartyTable::GetFilename (computes AVAILNPC%d; ruled out as the PC source)0x00563620
CSWPartyTable::UpdateMembers0x00565530
CSWSPlayer::SaveServerCharacter (multiplayer server-vault BIC writer; ruled out)0x005624c0
CSWPartyTable::AddGameInProgress / RemoveGameInProgress (reference-counted mount/unmount of the GAMEINPROGRESS: scratch directory)0x005638d0 / 0x00563950
CServerExoAppInternal::StopServices (nukes GAMEINPROGRESS: at session teardown – the only place a stray PC file is ever cleared)0x004b7e25
CServerExoAppInternal::DoModuleEnterSaveCleanup (prunes stale nested-module entries in GAMEINPROGRESS:; unrelated to PC, recorded to clarify what does and doesn’t get cleaned there)0x004b23ea
CServerExoAppInternal::LoadModule (a second LoadModule address found in this pass; the address recorded earlier in this table for CSWSModule::LoadModule, 0x004b95b0, was found in an earlier session under a different class-qualified name – likely a caller/callee pair rather than a conflict, not reconciled)0x004b98e0

Located while tracing the .rsv intermediate module format through the extension table, save-size estimation, and the load path (CExoBaseInternal::GetResourceExtension was already in this table from an earlier pass, at the same address, confirmed as the same function):

FunctionAddress
CExoBaseInternal::CreateResourceExtensionTable0x005e6d20
CExoBaseInternal::GetResTypeFromExtension0x005e7a40
CExoBase::GetResTypeFromExtension (thin forwarder to the Internal version above)0x005e6670
CServerExoAppInternal::SetEstimatedSaveSize0x004b5f90
CExoResMan::GetResTypeFromFile0x00406650

Located while tracing the Gold read gate through the benched-companion rejoin path:

FunctionAddress
CSWPartyTable::AddMember0x00565620
CSWGuiPartySelection::AcceptParty0x006be560
CSWVirtualMachineCommands::ExecuteCommandSpawnAvailableNPC0x00543ed0