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
| Property | Value |
|---|---|
| On-disk unit | A directory under the SAVES: alias, named NNNNNN - <name> (for example, 000231 - Game230); slots 000000 and 000001 are reserved, see below |
| Main archive | SAVEGAME.sav, an ERF with version tag MOD V1.0 |
| Loose sidecars | savenfo.res, PARTYTABLE.res, GLOBALVARS.res (each a GFF), plus Screen.tga |
| Rust reference | rakata-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.
The save folder layout
A vanilla save folder contains:
| File | Format | Role |
|---|---|---|
SAVEGAME.sav | ERF (MOD V1.0) | The bundle: every per-module archive plus the global session resources |
savenfo.res | GFF (NFO ) | Menu metadata: name, area, last module, play time, portraits |
PARTYTABLE.res | GFF (PT ) | Party roster, gold, XP, journal, available companions, pazaak, galaxy map |
GLOBALVARS.res | GFF (GVT ) | Campaign global variables (booleans, numbers, locations, strings) |
Screen.tga | TGA | Save-slot preview thumbnail |
These metadata files sit loose so the load menu can read them straight from the slot, with no archive opened: a name, area, play time and thumbnail for every save. SAVEGAME.sav grows with every module you visit and runs to megabytes late in a game, while savenfo.res stays a few hundred bytes, so listing a folder of saves costs the same whenever you do 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 the IncludeInSave column of modulesave.2da, finding the row by the module’s name as a string, and excludes a module only when that row exists and holds 0.
Fail-open holds on every failure path, traced through CServerExoAppInternal::IncludeModuleInSave (0x004b2121). A table that will not allocate, a load that fails, and a row or column that does not resolve all fall through to including 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:
| Resource | Type | What it holds |
|---|---|---|
IFO (resref Module) | module info | The 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. It uses the same Mod_PlayerList container as a module roster, and its entries are not the same shape. Both hold the same creature serialization under struct id 48813, but SavePlayers wraps a module-roster entry in the header fields that StorePlayerCharacters never writes. A pifo entry is the bare snapshot.
Each player is stamped with the roster slot it was written to, on the live object rather than in the file. Nothing in a pifo entry records it, so the stamp does not outlive the session that made it.
The sentinel index 0xffffffff is what selects pifo. LoadCharacterFromIFO normally reads a slot out of the module’s own Module IFO; that sentinel switches it to pifo. Its one call site is LoadCharacterStart, and the module-roster path passes a real slot index instead. The branch is reached only when the player object already carries a slot from an earlier write, which is why an ordinary manual load never takes it, and why a manual save folder carries no pifo.ifo to read.
So 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.
Do not rely on a Player.bic being present. The engine can write the primary player as a standalone Player file in the BIC character format, a SaveCreature snapshot behind a small header written by SavePrimaryPlayerInfo, but that path is gated behind a global flag and does not run for ordinary single-player saves.
There is no matching .bic reader. On load the primary player comes from the module roster: LoadPrimaryPlayer takes the module’s primary-player index and hands off to LoadCharacterFinish. Any Player record is consumed through the same Mod_PlayerList creature-load path as a roster member, keyed by ObjectId.
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 (GetNPCObjectapplies 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’sPercentXPfor their row, applied toPT_XP_POOLfrom 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.
Neither side bounds how many items it holds. The writer walks the repository’s own item array on a full-width index and the reader walks the GFF list count the same way, with no ceiling compared against in either loop. That is a different shape from the party-table counts nearby, which cap at 255 because the count is stored as a byte; an inventory’s count comes from the GFF list header and is never narrowed, so there is no width to run out of. A save carrying exactly 255 items is a coincidence of that player’s stash rather than the engine stopping there.
Gold and the party pool
Party wealth is one number, PT_GOLD in PARTYTABLE.res, but every creature’s save block still carries its own Gold field. The in-party flag that SetInParty maintains is what keeps the two from fighting, and it is used on both sides of the cycle.
Writing. SaveCreature briefly clears the flag around its call into SaveStats. The gold accessors read that flag to decide whether “this creature’s gold” means a private ledger or the shared pool, so clearing it captures the member’s frozen personal value rather than the live party total.
Reading. ReadStatsFromGff uses the same flag as a gate: a creature currently in the party skips its Gold field entirely. Without that, each member’s stale snapshot would clobber PT_GOLD as the blocks loaded one after another. The pool is authoritative and the per-member values ride along.
A creature that was never in the party is unaffected. 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 gold is a one-shot amount
AVAILNPCn snapshots land in that same clear-flag case, by a route worth following. GetNPCObject constructs a fresh CSWSCreature, whose flag starts clear like any new object, and loads the companion’s GFF through LoadFromTemplate before anything sets it. So a rejoining companion’s Gold reads back normally rather than being skipped.
It does not stay personal. AddMember calls TransferInventory on the freshly spawned creature, folding that just-loaded Gold into PT_GOLD and moving their items into the party stash, before the flag is set to 1. First-time recruitment goes the same way through AddNPC, so a companion’s starting gold from their .utc template enters the pool the moment they are recruited.
The stray PC file: a party-leader-swap artifact, not a save artifact
A save can carry a bare PC resource: a UTC-shaped GFF in the same raw format as an AVAILNPCn snapshot. It is a transient swap buffer, not a save mechanism. CSWPartyTable::SwitchPlayerCharacter writes it to GAMEINPROGRESS:PC when module content hands control away from the born player character, and reads it back when control returns.
It reaches SAVEGAME.sav by accident. StallEventSaveGame and DoPCAutosave both import the entire working directory into the ERF unconditionally, sweeping up whatever is sitting there, and nothing clears GAMEINPROGRESS: between saves. The directory is wiped only at session teardown, in CServerExoAppInternal::StopServices, when you quit to the menu or load a different game.
So once a leader swap has fired anywhere in a session, PC is archived into every save taken afterward, quicksave, manual and autosave alike.
That rules it out as a save-type discriminator. Its presence tracks session history rather than which writer produced the file. It is also unrelated to the pifo roster-staging file and the gated Player BIC record above; all three reuse the SaveCreature serializer for otherwise unconnected purposes.
Building and reading SAVEGAME.sav
At save time the engine imports the entire GAMEINPROGRESS: working directory into one ERF, through StallEventSaveGame and CERFFile::ImportFiles. The per-module ERFs already live there, so they land inside SAVEGAME.sav as nested resources keyed by module resref under resource type 2057 (sav).
The flat session resources that accumulate in the same directory come along with them: the REPUTE faction table, the AVAILNPC companion snapshots, and the party INVENTORY, which is stored under the generic resource type 0 and so has to be looked up by name.
That import is a blanket sweep, not a curated list, so an incidental leftover like the PC file above rides along too. Treat those three named resources as what you can rely on rather than as a closed inventory of what a SAVEGAME.sav contains.
Reading a module’s state back is a two-level walk:
- Open
SAVEGAME.sav. - Find the resource named after the module.
- Parse that resource as its own ERF, then read the
GITinside 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:
- If the module exists as an
RSVresource, the engine resolves the filename through theGAMEINPROGRESS:alias and opens it as typeRSV(0x0bc1). - If
RSVis not found, it falls back toSAV(type0x0809, decimal2057, 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_fileis not affected by whether a module loaded asRSVorSAV. It comes from one place only: the module’s ownMod_IsNWMFileIFO field, read the same way regardless of resource type (see the NWM note above). ARE loading is unconditional too. The area object always demands itsAREresource as the first step of loading, with no code path that skips it for any resource type. A hand-staged.rsvstill needs a validAREto load successfully; it cannot get by onIFOandGITalone.
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 is not type-keyed the way it sounds:
- A file that fails to open gets a flat 2.5 MB stand-in, whatever its type, rather than being skipped.
- Once every file is summed, a further flat 2.5 MB is added, but only when the module currently loading is neither
RSVnorSAV. Loading from either is exempt from that top-up. - An empty
GAMEINPROGRESSskips both and uses a fixed 3.75 MB baseline. - The total is then scaled up by roughly 11% before being stored.
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), and a staged one is indistinguishable from an engine-written one once it is in there.
So .rsv is the working-directory format and sav is the archive type. A tool reading SAVEGAME.sav never meets the extension. A tool reading GAMEINPROGRESS directly, to inspect a session with no committed save, must look for <module>.rsv: same content, different extension and resource type.
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 / quicksave | Autosave | |
|---|---|---|
| Writer | StallEventSaveGame | DoPCAutosave |
| Fires when | you save to a slot, or quicksave | you cross into a new module (StartNewModule) |
savenfo-only fields | SAVEGAMENAME, LIVE1-LIVE6, LIVECONTENT | PCAUTOSAVE (=1), SCREENSHOT, AUTOSAVEPARAMS |
| Slot thumbnail | Screen.tga (captured frame) | none; SCREENSHOT holds a load_<module> resref |
pifo.ifo | absent | present |
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.
The autosave writer places that pifo.ifo itself, and pifo’s presence is as reliable a tell as PCAUTOSAVE is. The scratch copy at TEMP:pifo is written on every module exit, whatever kind of save follows, so its existence says nothing. What separates the two writers is that DoPCAutosave copies it into the save folder and StallEventSaveGame never touches it. No filter is involved: TEMP: is a separate resource alias from the GAMEINPROGRESS: directory the writers sweep, so the scratch file was never a candidate for the archive.
The scratch copy is consumed on module entry and not deleted. The reader adds TEMP: to the search path, reads the roster out, and removes it again, leaving the file to be overwritten by the next exit. So a manual save taken immediately after a transition still has no pifo.ifo beside it, not because the scratch file is gone but because its writer never copies one.
For a tool that means a folder where pifo’s presence and PCAUTOSAVE disagree has been rearranged by something other than the game. Both come from the same writer, so they cannot legitimately come apart.
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.
| Slot | Folder | Kind |
|---|---|---|
000000 | 000000 - QUICKSAVE | Quicksave |
000001 | 000001 - AUTOSAVE | Autosave |
000002 and up | 000002 - 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.
The reserved numbers are hardcoded literals rather than anything derived from scanning folders, and the autosave loader, the save-menu filter and the quickload finder each compare a parsed slot number against them independently. The set of names that can reach the formatter is closed, established by walking every caller down to the shared write backend (CServerExoAppInternal::SaveGame) and the shared unpack/copy helpers:
| Name | Slot number | Writer |
|---|---|---|
QUICKSAVE | 0 (literal) | CClientExoAppInternal::DoQuickSave, via the generic SaveGame backend |
AUTOSAVE | 1 (literal) | DoPCAutosave (self-contained), and separately the periodic-autosave branch in CServerExoAppInternal::MainLoop, which calls the same generic SaveGame backend directly |
| the player-entered save name | 2 and up (allocation mechanism below) | The manual-save flow, through the same generic SaveGame backend |
No fourth name reaches it, and nothing in the traced call graph reserves a number below 2.
Note
REBOOTAUTOSAVEis not a folder name and never reaches the slot-name formatter. It is a boolean byte insidesavenfo.res, besidePCAUTOSAVE, read unconditionally by the load-menu’s slot parser. That read is its only cross-reference in the binary.Nothing writes it.
DoPCAutosavesetsPCAUTOSAVEalone. The read path is live: the save-list preview checks this bit alongsidePCAUTOSAVEwhen deciding where to pull a slot’s screenshot from. But with no producer on PC the field is permanently0in every save this build creates.The name suggests a hard-reset or dashboard-return autosave, most likely inherited console-SKU logic parsed tolerantly here, the same shape as the
LIVE%dXbox content mounts. (Provenance: inferred.)
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 the gap search also comes up empty, with 2 through 999 all occupied, 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.ifoexists because at a transition the party is staged in the transientpifoparty-info file rather than a module roster (the same file the0xffffffffload path reads, see The party roster);DoPCAutosavecopies it into the folder. It is a GFF taggedIFOholding aMod_PlayerListof the party.- No
Screen.tgabecause there is no gameplay frame to capture mid-loading-screen, so the autosave records the loading-screen resref inSCREENSHOTinstead. (The slot thumbnail is independent of theEnableScreenShotini 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 label | Object | Notes |
|---|---|---|
Creature List | creatures | player characters are split out into a separate player list, not this one |
List | item instances | items in the area use the bare label List |
Door List | doors | |
TriggerList | triggers | |
Encounter List | encounters | |
WaypointList | waypoints | |
SoundList | sounds | |
Placeable List | placeables | corpses are excluded |
StoreList | stores | |
AreaEffectList | area-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.
| Aspect | UseTemplates = 1 (static .git) | UseTemplates = 0 (savegame GIT) |
|---|---|---|
| Object element | sparse placement | full self-contained snapshot |
TemplateResRef | present | absent (not read) |
| Blueprint load | yes, via the object’s LoadFromTemplate (UTC/UTD/UTP/UTT/…), then instance fields overlaid | none; the engine reads every field directly (CSWSCreature::LoadCreature, CSWSDoor::LoadDoor, …) |
| Where the data lives | mostly in the blueprint | entirely in the GIT element |
| A field missing from the element | comes from the blueprint | comes 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 = 0object 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.
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”, read 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 byUseTemplates. 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 behaviour | What the engine does |
|---|---|
| Field names vary by object type | No 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 things | A 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 full | Creatures, 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-coupled | Trigger 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 restored | MaxHitPoints, 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 reload | Written 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:
- The load menu reads the loose
savenfo.resstraight from each slot for its name, area, play time, and thumbnail; no archive is opened. When you pick a slot, itsLASTMODULEnames the first module to restore. CopyGameToFutureGameunpacks the chosen slot, theSAVEGAME.savERF plus the loose sidecars, into theFUTUREGAME:staging area. It callsCERFFile::ReadHeaderVarianceand thenExportFilesFromERF, which writes one file per archive entry, taking each filename from the entry’s key and its bytes from the resource at the same table index. See what a writer must preserve.- The engine clears
GAMEINPROGRESS:and renamesFUTUREGAME:onto it. The live session runs from this unpacked copy. CSWSModule::LoadModulereplays the module against the working directory, runningLoadModuleStart/LoadModuleInProgress/LoadModuleFinish(the inverse ofStoreCurrentModule).LoadModuleStartreads the per-moduleIFOincluding its save-only fields, loads the globalREPUTEfaction table (LoadFactionsFromSaveGame/LoadReputationsFromSaveGame), and reads theGITwithUseTemplates = 0, so every object comes from its full snapshot.- 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
CORRUPTmarker 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.
No checked validation can reach that branch. None of the ordinary return-code paths in the ERF-reading chain signals failure for a missing, truncated or garbled file. That chain runs CERFFile::Read, ReadHeaderVariance and ExportFilesFromERF down to the raw read and write calls; they tolerate short reads with a retry-and-log, and CopyGameToFutureGame has a single return path, an unconditional success.
So the marker can only be written if a genuine exception unwinds out of the copy, and the functions in this chain do install real C++ exception handling.
(Provenance: inferred.) The likeliest trigger, going by what the code does with the data, is a corrupted archive header’s entry or language counts driving an allocation or a read past what is there. There is no explicit corruption check anywhere in this path to name as the validation.
How the load menu treats a corrupt slot
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: no code deletes CORRUPT.res specifically. 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.0ERF 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 theGITinside it. - Check
UseTemplatesfirst. A savegame GIT (UseTemplates = 0) is self-contained, so read objects directly. A module’s static.git(UseTemplates = 1) is template-relative, so resolve eachTemplateResRefagainst 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.
Key Ghidra addresses
Function addresses in swkotor.exe (K1 GOG build), for anyone continuing this archaeology. Where a name appears twice the rows say why.
| Function | Address |
|---|---|
CServerExoAppInternal::StallEventSaveGame | 0x004b3110 |
CServerExoAppInternal::SaveGame | 0x004b58a0 |
CServerExoAppInternal::DoPCAutosave | 0x004b8300 |
CServerExoAppInternal::StartNewModule | 0x004ba920 |
CServerExoAppInternal::StoreCurrentModule | 0x004b2e70 |
IncludeModuleInSave | 0x004b20e0 |
CSWSModule::SaveModuleStart | 0x004c8960 |
CSWSModule::SaveModuleInProgress | 0x004c3b10 |
CSWSModule::SaveModuleFinish | 0x004ca680 |
CSWSModule::SavePrimaryPlayerInfo | 0x004c3c70 |
CSWSModule::SavePlayers | 0x004c7870 |
CSWPartyTable::Save | 0x005665c0 |
CSWPartyTable::SaveTableInfo | 0x005648c0 |
CSWPartyTable::SaveJournal | 0x00563d90 |
CSWPartyTable::AddNPC | 0x00564300 |
CSWPartyTable::SaveMember | 0x00563e80 |
CSWPartyTable::UpdateInventory | 0x00564030 |
CSWPartyTable::GetNPCObject | 0x00564700 |
CSWPartyTable::CreateParty | 0x00565760 |
CSWPartyTable::SpawnNPC | 0x00565130 |
CSWGlobalVariableTable::Save | 0x0052ad10 |
CSWGlobalVariableTable::WriteTable | 0x005299b0 |
CSWSArea::SaveGIT | 0x0050ba00 |
CSWSArea::SaveCreatures | 0x00507680 |
CSWSArea::SaveDoors | 0x00507810 |
CSWSArea::SaveTriggers | 0x005078d0 |
CSWSArea::SavePlaceables | 0x00507bd0 |
CSWSDoor::SaveDoor | 0x00588ad0 |
CSWSPlaceable::SavePlaceable | 0x00586a70 |
CSWSTrigger::SaveTrigger | 0x0058e660 |
CSWSCreature::SaveCreature | 0x00500610 |
CSWSCreatureStats::SaveStats | 0x005b1b90 |
CSWSWaypoint::SaveWaypoint | 0x005c8230 |
CSWSStore::SaveStore | 0x005c6cd0 |
CSWSSoundObject::Save | 0x005c86d0 |
CSWSEncounter::SaveEncounter | 0x00591350 |
CSWSAreaOfEffectObject::SaveEffect | 0x00594d80 |
CResGFF::CreateGFFFile | 0x00411260 |
CSWSArea::LoadGIT | 0x0050dd80 |
CSWSArea::LoadCreatures | 0x00504a70 |
CSWSCreature::LoadCreature | 0x00500350 |
CSWSCreature::LoadFromTemplate | 0x005026d0 |
CSWSTrigger::LoadTrigger | 0x0058da80 |
LoadTriggers | 0x0050a350 |
LoadTriggerGeometry | 0x0058d060 |
CSWGuiSaveLoad::UnpackGame | 0x006caaf0 |
CopyGameToFutureGame | 0x006c9a90 |
CSWSModule::LoadModule | 0x004b95b0 |
CSWSModule::LoadModuleStart | 0x004c9050 |
CServerExoAppInternal::LoadPrimaryPlayer | 0x004b5f50 |
CServerExoAppInternal::LoadCharacterStart | 0x004b7470 |
CServerExoAppInternal::LoadCharacterFinish | 0x004b5c50 |
CServerExoAppInternal::StorePlayerCharacters | 0x004b2ba0 |
CSWSPlayer::LoadCharacterFromIFO | 0x00561e30 |
CSWGlobalVariableTable::ReadTableWithCatalogue | 0x0052a280 |
CSWGlobalVariableTable::GetValueLocation | 0x00529350 |
CSWSObject::GetScriptLocation | 0x004cb7b0 |
CFactionManager::LoadFactionsFromSaveGame | 0x0052b5c0 |
CFactionManager::LoadReputationsFromSaveGame | 0x0052bbe0 |
CSWSCreatureStats::ReadStatsFromGff | 0x005afce0 |
CSWSCreatureStats::ReadSpellsFromGff | 0x005aeb30 |
CSWSCreatureStats::SaveClassInfo | 0x005aec90 |
CSWSCreatureStats::CSWSCreatureStats (constructor) | 0x005aca80 |
CCombatInformation::LoadData | 0x00552350 |
CCombatInformation::SaveData | 0x00550f30 |
CSWSCombatRound::LoadCombatRound | 0x004d5120 |
CSWSPlayer::LoadCreatureData | 0x00560e60 |
CSWSMessage::SendServerToPlayerUpdateCharResponse | 0x00570c60 |
CSWSCreature::ReadScriptsFromGff | 0x004ebf20 |
CSWSCreature::LoadFollowInfo | 0x004fb180 |
CSWSCreaturePartyFollowInfo::Load | 0x004eb020 |
CSWSCreaturePartyFollowInfo::Save | 0x004eaf70 |
CSWSCreaturePartyFollowInfo::CSWSCreaturePartyFollowInfo (constructor) | 0x004f79e0 |
CSWSObject::LoadListenData | 0x004d0480 |
CSWSObject::SaveListenData | 0x004cca50 |
CSWSObject::LoadObjectState | 0x004d1cf0 |
CSWSObject::SaveObjectState | 0x004cec50 |
CSWSObject::LoadEffectList | 0x004d1be0 |
CSWSObject::SaveEffectList | 0x004cc9d0 |
CGameEffect::LoadGameEffect | 0x005043a0 |
CSWSObject::LoadActionQueue | 0x004cecb0 |
CSWSObject::SaveActionQueue | 0x004cc7e0 |
CSWSObject::CSWSObject (base constructor) | 0x004cfcb0 |
CSWSScriptVarTable::LoadVarTable | 0x0059aa80 |
CSWSScriptVarTable::SaveVarTable | 0x0059adb0 |
CSWVarTable::LoadVarTable | 0x0059b0f0 |
CSWVarTable::SaveVarTable | 0x0059b250 |
CSWSCreature::ReadItemsFromGff | 0x004ffda0 |
CSWSCreature::CSWSCreature (constructor) | 0x004f7a10 |
CSWSCreature::SetDetectMode | 0x0050ee30 |
CSWSCreature::SetStealthMode | 0x0050ee50 |
CSWSModule::LoadLimboCreatures | 0x004c8c70 |
CSWSModule::SaveLimboCreatures | 0x004c5bb0 |
CSWSModule::LoadModuleInProgress | 0x004c5720 |
CSWSArea::LoadArea | 0x0050e190 |
CItemRepository::GetItemRepository | 0x004ef770 |
CSWSItem::LoadItem | 0x00560970 |
CSWSItem::LoadFromTemplate | 0x005608b0 |
CSWSItem::LoadDataFromGff | 0x0055fcd0 |
CSWSItem::CSWSItem (constructor) | 0x005530a0 |
CSWItem::CSWItem (base constructor) | 0x005b4660 |
CSWSItem::SetPossessor | 0x00553210 |
CSWSItem::SaveItem | 0x0055ccd0 |
CSWSItem::SaveItemProperties | 0x00555790 |
CSWSItem::SaveContainerItems | 0x0055cfa0 |
CSWSItem::ReadContainerItemsFromGff | 0x0055f0f0 |
CSWSArea::LoadItems | 0x00504de0 |
CSWSArea::SaveItems | 0x00507750 |
CSWSDoor::LoadDoor | 0x0058a1f0 |
CSWSDoor::LoadFromTemplate | 0x0058b3d0 |
CSWSDoor::LoadDoorExternal | 0x0058c5f0 |
CSWSDoor::CSWSDoor (constructor) | 0x00589ee0 |
CSWSDoor::PostProcess | 0x00589d40 |
CSWSArea::LoadDoors | 0x0050a0e0 |
CSWSPlaceable::LoadPlaceable | 0x00585670 |
CSWSPlaceable::LoadFromTemplate | 0x00587a70 |
CSWSPlaceable::CSWSPlaceable (constructor) | 0x005877e0 |
CSWSPlaceable::LoadBodyBag | 0x005864b0 |
CSWSPlaceable::SpawnBodyBag | 0x004ce220 |
CSWSPlaceable::AcquireItem | 0x00584b10 |
CSWSPlaceable::PostProcess | 0x00584870 |
CSWSArea::LoadPlaceables | 0x0050a7b0 |
ExecuteCommandCreateObject | 0x0052f820 |
CSWSTrigger::CSWSTrigger (constructor) | 0x0058eae0 |
CSWSTrigger::LoadFromTemplate | 0x0058ed70 |
CSWSTrigger::AddToArea | 0x0058f030 |
CSWSWaypoint::LoadWaypoint | 0x005c7f30 |
CSWSWaypoint::CSWSWaypoint (constructor) | 0x005c7e70 |
CSWSArea::LoadWaypoints | 0x00505360 |
CSWSStore::LoadStore | 0x005c7180 |
CSWSStore::LoadFromTemplate | 0x005c7760 |
CSWSStore::CSWSStore (constructor) | 0x005c6ab0 |
CSWSStore::AddItemToInventory | 0x005c70c0 |
CSWSArea::LoadStores | 0x005057a0 |
CSWSSoundObject::Load | 0x005c9040 |
CSWSSoundObject::LoadFromTemplate | 0x005c94e0 |
CSWSSoundObject::CSWSSoundObject (constructor) | 0x005c8f30 |
CSWSArea::LoadSounds | 0x00505560 |
CSWSEncounter::ReadEncounterFromGff | 0x00592430 |
CSWSEncounter::ReadEncounterScriptsFromGff | 0x00590820 |
CSWSEncounter::LoadEncounter | 0x00593830 |
CSWSEncounter::LoadFromTemplate | 0x00593a90 |
CSWSEncounter::LoadEncounterGeometry | 0x00590580 |
CSWSEncounter::LoadEncounterSpawnPoints | 0x00590410 |
CSWSEncounter::CSWSEncounter (constructor) | 0x00593c70 |
CSWSArea::LoadEncounters | 0x00505060 |
CSWSAreaOfEffectObject::LoadEffect | 0x00594b00 |
CSWSAreaOfEffectObject::CSWSAreaOfEffectObject (constructor) | 0x00594480 |
CSWSArea::LoadAreaEffects | 0x00505af0 |
CSWSArea::LoadProperties | 0x00507490 |
CSWSArea::SaveProperties | 0x00506090 |
CSWSArea::LoadMaps | 0x00505da0 |
CSWSArea::SaveMaps | 0x005061d0 |
CSWSArea::LoadPlaceableCameras | 0x00505eb0 |
CSWSArea::SavePlaceableCameras | 0x005062a0 |
CSWSModule::SaveModuleIFOStart | 0x004c7050 |
CSWSModule::SaveModuleIFOFinish | 0x004c8b90 |
CSWSModule::SaveStatic | 0x004c5980 |
CSWSAmbientSound::CSWSAmbientSound (constructor located; Load/Save not individually decompiled) | 0x005c95a0 |
MainLoop (two call sites feed LoadModuleStart/limbo-creature handling) | 0x004babb0, 0x004ae860 |
CSWSCreatureStats::GetFortSavingThrow | 0x005ab810 |
CSWSCreatureStats::GetWillSavingThrow | 0x005ab880 |
CSWSCreatureStats::GetReflexSavingThrow | 0x005ab8f0 |
CSWSCreatureStats::GetBaseFortSavingThrow | 0x005aa1b0 |
CSWSCreatureStats::GetBaseWillSavingThrow | 0x005aa2f0 |
CSWSCreatureStats::GetBaseReflexSavingThrow | 0x005aa430 |
CSWSCreature::GetArmorClass | 0x004ed1d0 |
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::GetMaxHitPoints | 0x004d01a0 |
CSWSCreature::GetMaxHitPoints | 0x004ed310 |
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::GetGold | 0x004edd60 |
CSWSCreature::SetGold | 0x004edda2 |
CSWSCreature::AddGold | 0x004f3dc8 |
CSWSCreature::RemoveGold | 0x004f3eea |
CSWSCreature::TransferGold | 0x004fd769 |
CSWSCreature::SetInParty | 0x004fdb2d |
CSWSCreature::CSWSCreature (a second constructor address; 0x004f7a10 appears elsewhere in this table under the same name. Likely a different overload, not reconciled) | 0x004f7b47 |
CSWSPlayer::LoadLocalCharacter | 0x00561d70 |
ExecuteCommandAddPartyMember | 0x0052de70 |
ExecuteCommandRemovePartyMember | 0x00541c00 |
SwitchPlayerCharacter | 0x005667c0 |
TransferInventory | 0x005641e0 |
CSWSAreaOfEffectObject::LoadAreaEffect (singular; the vfx_persistent.2da-driven definition lookup, wired only to fresh spell-cast creation, never to a save load) | 0x005947b0 |
CSWSEffectListHandler::OnApplyAreaOfEffect | 0x004dade0 |
ApplyEffect | 0x0050c6b0 |
CSWSAreaOfEffectObject::AIUpdate (heartbeat tick) | 0x00595d10 |
CSWSAreaOfEffectObject::EventHandler (enter/exit collision events) | 0x005964e0 |
CVirtualMachineInternal::RunScript | 0x005d45d0 |
CVirtualMachine::RunScript (thin forwarder) | 0x005d0fc0 |
CSWSAreaOfEffectObject::GetEffectSpellId / SetEffectSpellId | 0x005945d0 / 0x005945e0 |
CClientExoAppInternal::DoQuickSave | 0x005f4b50 |
CSWGuiSaveLoadEntry::LoadData | 0x006c8e50 |
CSWGuiSaveLoad::LoadPCAutoSave | 0x006ca250 |
CSWGuiMainMenu::OnPanelAdded (disk-space probe reusing the reserved AUTOSAVE name) | 0x0067b6c0 |
CSWGuiSaveLoad::PopulateGameList | 0x006cc160 |
CSWGuiSaveLoad::HandleSaveButton | 0x006cbb60 |
CSWGuiSaveLoad::PromptForSaveName | 0x006cb820 |
CSWGuiSaveLoad::WriteGame | 0x006c8790 |
CSWGuiSaveLoad::ShowGame | 0x006c89d0 |
CSWGuiSaveLoadEntry::SetXboxTitle | 0x006c9780 |
CGuiInGame::DoQuickLoad | 0x00633c50 |
CSWGuiSaveLoadEntry::CSWGuiSaveLoadEntry (constructor) | 0x006cb940 |
KOTOR_AUTOSAVE_PARAMS::SaveToGFF | 0x004b28e0 |
CStatusSummary::SaveToGFF | 0x004b26c0 |
CGuiInGame::GetStatusSummary / SetStatusSummary | 0x0062f0a0 / 0x0062f040 |
CGuiInGame::SuppressStatusSummary | 0x0062f0c0 |
CGuiInGame::GetPendingStatusSummary | 0x0062ef70 |
CGuiInGame::ShowStatusSummary | 0x0062ef90 |
CGuiInGame::UpdateStatus | 0x0062eeb0 |
CSWGuiStatusSummary::AddAlignmentShift | 0x00624a70 |
CSWGuiStatusSummary::AddCredits | 0x00624ab0 |
CSWGuiStatusSummary::AddXp | 0x0062b580 |
CSWGuiStatusSummary::AddStealthXp | 0x0062b5a0 |
CSWVirtualMachineCommands::ExecuteCommandSuppressStatusSummaryEntry | 0x00547e50 |
CSWVirtualMachineCommands::ExecuteCommandStartNewModule | 0x00544390 |
CClientExoApp::GetMoveToModuleMovies | 0x005edb60 |
CClientExoApp::AddMoveToModuleMovie | 0x005edb50 |
CClientExoApp::RemoveMoveToModuleMovies | 0x005ee380 |
CServerExoApp::GetMoveToModuleStartWaypoint / SetMoveToModuleStartWaypoint | 0x004aed40 / 0x004aed30 |
CServerExoApp::SetMoveToModulePending | 0x004aecc0 |
CServerExoApp::SetMoveToModuleString | 0x004aecd0 |
CClientExoApp::SetLoadScreenByModuleName | 0x005edcf0 |
CClientExoApp::GetLoadMusicByModuleName (thunk) / CClientExoAppInternal::GetLoadMusicByModuleName (implementation) | 0x005edd00 / 0x005f3650 |
CWorldTimer::GetWorldTime | 0x004ade40 |
CWorldTimer::ConvertFromTimeOfDay | 0x004add90 |
CSWSModule::GetTime | 0x004c4100 |
CGuiInGame::UnpackQuickSaveGame | 0x006323a0 |
CopyQuickSaveGameToFutureGame (quicksave counterpart to CopyGameToFutureGame; not fully decompiled, assumed the same shape by symmetry) | 0x0062fbe0 |
CERFFile::Read | 0x005dce50 |
CERFFile::ReadHeaderVariance | 0x005dd3c0 |
CERFFile::ExportFilesFromERF | 0x005dd710 |
CERFRes::CopyToFile | 0x005dd170 |
CExoFile::FileOpened / Read / Write | 0x005e6a10 / 0x005e6960 / 0x005e69a0 |
CExoFileInternal::Read / Write | 0x005eba40 / 0x005ebc60 |
CSWGuiSaveLoad::VerifyLoadGame | 0x006cc0e0 |
CSWGuiSaveLoad::LoadGame | 0x006cb0e0 |
CExoResMan::CleanDirectory | 0x00409460 |
CExoResMan::WipeDirectory | 0x00408e90 |
CExoAliasListInternal::ResolveFileName | 0x005eb6b0 |
CExoBaseInternal::GetResourceExtension | 0x005e7a00 |
CSWGuiSaveLoadEntry::GetGameDirectory | 0x006c8250 |
CServerExoApp::SaveGame (thin wrapper over CServerExoAppInternal::SaveGame, dispatched from network message handlers) | 0x004ae6e0 |
HandlePlayerToServerModuleMessage / HandleServerAdminToServerMessage | 0x00524800 / 0x00528380 |
CSWGuiSaveLoad::DeleteGame | 0x006caa90 |
ExecuteCommandSwitchPlayerCharacter (nwscript action dispatch, sole caller of SwitchPlayerCharacter) | 0x00544910 |
CSWPartyTable::GetFilename (computes AVAILNPC%d) | 0x00563620 |
CSWPartyTable::UpdateMembers | 0x00565530 |
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; CSWSModule::LoadModule at 0x004b95b0 appears elsewhere in this table under that different class-qualified name. Likely a caller/callee pair rather than a conflict, not reconciled) | 0x004b98e0 |
CExoBaseInternal::CreateResourceExtensionTable | 0x005e6d20 |
CExoBaseInternal::GetResTypeFromExtension | 0x005e7a40 |
CExoBase::GetResTypeFromExtension (thin forwarder to the Internal version above) | 0x005e6670 |
CServerExoAppInternal::SetEstimatedSaveSize | 0x004b5f90 |
CExoResMan::GetResTypeFromFile | 0x00406650 |
CSWPartyTable::AddMember | 0x00565620 |
CSWGuiPartySelection::AcceptParty | 0x006be560 |
CSWVirtualMachineCommands::ExecuteCommandSpawnAvailableNPC | 0x00543ed0 |