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.
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:
| 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, 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:
| 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 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 (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.
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:
- 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 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 / 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.
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.
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:
| Name | Slot number | Writer |
|---|---|---|
QUICKSAVE | 0 (literal) | CClientExoAppInternal::DoQuickSave, via the generic SaveGame backend |
AUTOSAVE | 1 (literal) | DoPCAutosave (self-contained), and separately MainLoop’s periodic-autosave branch, 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 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
REBOOTAUTOSAVEnever reaches the slot-name formatter at all – it isn’t a folder name. It’s a boolean byte field insidesavenfo.resitself (alongsidePCAUTOSAVE), 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;DoPCAutosaveonly ever setsPCAUTOSAVE. The save-list preview code checks this bit together withPCAUTOSAVEwhen it decides where to pull a slot’s screenshot from, so the read path is live. With no PC producer, though, 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 tolerantly parsed here for compatibility, the same shape as the already-documentedLIVE%dXbox content mounts. Two other REBOOT/AUTOSAVE-adjacent strings turned up during this trace and were ruled out as unrelated:CB_AUTOSAVEis an options-screen checkbox control id, andAutoSave/AutoSaveOnEnterare 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.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.
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 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.- 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.
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.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.
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
pifoparty-info file is a working GFF taggedIFO.LoadCharacterFromIFOreads it when the requested member index is0xffffffff. - A
Player.bichas no dedicated reader. The engine loads it through the ordinaryMod_PlayerListcreature-load path, keyed byObjectId. IncludeModuleInSavedecides which visited modules are written into the save, usingmodulesave.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):
| 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 |
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:
| Function | Address |
|---|---|
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 |
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):
| Function | Address |
|---|---|
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 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::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 |
Located while auditing save-slot numbering (QUICKSAVE/AUTOSAVE/REBOOTAUTOSAVE):
| Function | Address |
|---|---|
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 |
Located while enumerating AUTOSAVEPARAMS:
| Function | Address |
|---|---|
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 |
Located while tracing the CORRUPT.res marker:
| Function | Address |
|---|---|
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 |
Located while closing the reserved-name class (proving no fourth slot name reaches the formatter):
| Function | Address |
|---|---|
CSWGuiSaveLoadEntry::GetGameDirectory | 0x006c8250 |
CServerExoApp::SaveGame (thin wrapper over CServerExoAppInternal::SaveGame, dispatched from network message handlers) | 0x004ae6e0 |
HandlePlayerToServerModuleMessage / HandleServerAdminToServerMessage | 0x00524800 / 0x00528380 |
CSWGuiSaveLoad::DeleteGame | 0x006caa90 |
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):
| Function | Address |
|---|---|
ExecuteCommandSwitchPlayerCharacter (nwscript action dispatch, sole caller of SwitchPlayerCharacter) | 0x00544910 |
CSWPartyTable::GetFilename (computes AVAILNPC%d; ruled out as the PC source) | 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 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):
| Function | Address |
|---|---|
CExoBaseInternal::CreateResourceExtensionTable | 0x005e6d20 |
CExoBaseInternal::GetResTypeFromExtension | 0x005e7a40 |
CExoBase::GetResTypeFromExtension (thin forwarder to the Internal version above) | 0x005e6670 |
CServerExoAppInternal::SetEstimatedSaveSize | 0x004b5f90 |
CExoResMan::GetResTypeFromFile | 0x00406650 |
Located while tracing the Gold read gate through the benched-companion rejoin path:
| Function | Address |
|---|---|
CSWPartyTable::AddMember | 0x00565620 |
CSWGuiPartySelection::AcceptParty | 0x006be560 |
CSWVirtualMachineCommands::ExecuteCommandSpawnAvailableNPC | 0x00543ed0 |