Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

FAC Format (Faction & Reputation Table)

A .fac file is a single global table describing every faction in the game and how each faction feels about every other. The engine keys it on the resref REPUTE (type FAC ), and a save game stores the live, mutated copy under that resref. It is a flat GFF, not a per-object blueprint: there is one faction table for the whole session, not one per creature. Do not confuse it with repute.2da, a separate 2DA holding the static faction definitions a fresh game is seeded from (see Save-game context below).

At a Glance

PropertyValue
Extension(s).fac
Magic SignatureFAC / V3.2
TypeFaction & Reputation Table
Rust ReferenceNot yet modelled in rakata-generics.

File Layout

A FAC file is two top-level lists:

  • FactionList: the roster of factions, in faction-id order (a faction’s id is its index in this list).
  • RepList: a sparse set of pairwise reputation overrides between factions.

Individual objects (.utc/.utd/.utp/.ute) do not embed faction data; they carry a single Faction id that indexes into this shared table.

Field Schema

Every label the schema declares is tabulated at the end of this page, generated from the schema rather than written by hand.

Engine Audits & Decompilation

Read from the functions named below in swkotor.exe (K1 GOG build). Provenance: derived, not attested. The rows have not been separately re-derived, so they sit on the reverse-engineering queue. The field tables, the sparse-matrix rule, the reaction bands, and the global/personal model are read from:

FunctionAddressCovers
CFactionManager::SaveFactions0x0052b790FactionList write
CFactionManager::SaveReputations0x0052b830RepList write (only non-100 pairs emitted)
CFactionManager::LoadFactionsFromSaveGame0x0052b5c0FactionList read
CFactionManager::LoadReputationsFromSaveGame0x0052bbe0RepList read; the 100-baseline rebuild and 0-100 clamp
CFactionManager::GetIsNPCFaction0x0052b280Global vs personal faction model
CFactionManager::CreateDefaultFactions0x0052bce0Hardcoded default set used when no table loads
CFactionManager::LoadFactions0x0052b490Fresh-game path (repute.2da); origin of the FactionParentID sentinel, see below
ExecuteCommandGetNearestObject0x0054b550Reaction bands (0-10 / 11-89 / 90-100), corroborated by placeable/door/trigger usability checks

FactionList fields

FieldTypeMeaning
FactionNameCExoStringDisplay/lookup name of the faction.
FactionParentIDDWORDRead and round-tripped, but never consulted: see FactionParentID is a dead sentinel below.
FactionGlobalWORDWhether the faction is global. On load, a missing FactionGlobal defaults to 1.

A faction’s own id is its position in FactionList; it is not stored on the element.

Global versus personal factions. FactionGlobal flags whether a faction is one of the standard, shared factions (the set seeded from repute.2da) or a non-global one; when the field is absent on load it defaults to 1 (global). At the script layer, ChangeToStandardFaction moves a creature into a standard faction (the engine validates the target id with CFactionManager::GetIsNPCFaction and refuses otherwise), while ChangeFaction moves it into another creature’s faction; neither can change a player character’s faction. When no faction table loads at all, the engine falls back to a small hardcoded default set (CFactionManager::CreateDefaultFactions) rather than reading repute.2da.

FactionName defaults to a literal empty string when absent, unconditionally. An empty name still leaves the faction in the general roster. It is just never wired up as one of the manager’s special named roles (player, hostile_1, friendly_1, hostile_2, friendly_2, neutral, insane), since an empty string can’t match any of those six comparisons.

FactionParentID is a dead sentinel

Write 0xFFFFFFFF and never read it. Every faction in a real save carries that value, and nothing in the engine consults it: LoadFactionsFromSaveGame copies it onto the record with no comparison or branch, SaveFactions writes it straight back, and no reputation lookup, membership change or script-layer faction command touches it, nor anything else taking a CFactionManager* or CSWSFaction*. There is no parent-child traversal anywhere in this build.

It comes from LoadFactions, the fresh-game path that seeds factions out of repute.2da, which hardcodes 0xFFFFFFFF for every faction it creates. Nothing downstream changes it, so it survives every save and load after that: vestigial infrastructure for a hierarchy the engine never implements.

A file that omits the field loads as 0 instead, because the save-restore path reads through the ordinary ReadFieldDWORD with a 0 default while LoadFactions never calls a field reader at all. Since nothing reads either value the difference is cosmetic, but 0xFFFFFFFF is what every real save holds.

RepList fields and the sparse-matrix rule

Reputation is conceptually an N x N matrix (every faction’s standing toward every other faction), but it is stored sparsely. The default standing is 100 (the top of the friendly band, see below), and the writer emits a RepList entry only for pairs whose reputation is not 100.

FieldTypeMeaning
FactionID1DWORDSource faction id.
FactionID2DWORDTarget faction id.
FactionRepDWORDStanding of faction 1 toward faction 2, 0-100.

Important

A missing pair means 100, not zero. On load the engine first rebuilds the full reputation matrix at its default baseline (every pair starts at 100), then applies the RepList entries as overrides. FactionRep is clamped to 0-100 on load (values at or above 101 snap to 100, negatives snap to 0). A reader that treats absent pairs as 0 will make the whole galaxy hostile.

FactionID1 and FactionID2 Share a Default, Not a Consequence

Both default to a literal 0 when absent from a present RepList entry. The write is gated on an asymmetric bounds check, so that shared 0 does opposite things:

Missing fieldCheckOutcome
FactionID2must be strictly greater than 00 fails, and the entry’s write is skipped silently, as though the entry were not there
FactionID1only needs to be a valid index0 passes, and the entry is written using faction index 0 as the row

So an entry with a valid FactionID2 and a missing FactionID1 is not dropped. It overwrites whatever reputation pair sits at (faction 0, FactionID2). Same type, same default, opposite consequences.

FactionRep itself also defaults to a literal 0 when absent from a present, otherwise-valid entry, rather than the 100 sparse-matrix baseline documented above, which only applies when the whole pair is missing from RepList. A present entry with valid ids but no FactionRep writes a standing of 0 (hostile) into the matrix, clamped as a no-op since 0 is already in range, overwriting whatever baseline was sitting there.

There’s a second-order hazard worth knowing if you’re hand-editing a RepList: the loader reuses one found-flag variable across all three fields in an entry, and only checks it after FactionRep, the last of the three reads. A present FactionID1/FactionID2 with an absent FactionRep clears that shared flag, and the loader reads it as “this entry failed,” silently truncating every RepList entry that follows, not just the one with the missing field.

What the numbers mean

FactionRep is a 0-100 standing, and the engine reads it in three bands:

FactionRepReaction
0-10Hostile (treated as an enemy)
11-89Neutral
90-100Friendly (treated as a friend)

The same 10 and 90 boundaries turn up all over the engine: whether an NPC counts you as an enemy, whether a placed mine arms against you, and whether you may use a placeable, door, or trigger that a faction owns. So the default of 100 lands an unmodified pair squarely in the friendly band, not a literal midpoint.

Save-game context

There is no per-module faction file. When the engine stores a module it writes the entire global faction manager to a single REPUTE resource (type FAC ) in the GAMEINPROGRESS: working directory, which is then bundled into SAVEGAME.sav. So a save’s faction state lives at the resref REPUTE inside the main save archive, carrying the FactionList / RepList structure described above.

On load, LoadModuleStart probes for a REPUTE resource of type FAC . If one resolves (the save’s bundled copy, or a repute.fac that ships inside a module archive) it restores the table with LoadFactionsFromSaveGame / LoadReputationsFromSaveGame. If none is found, it builds the table from repute.2da instead and seeds the default reputations.

Note

repute.fac and repute.2da are two different resources, and only one of them is this format. repute.fac is the FAC GFF: the runtime/saved faction table (resref REPUTE, type FAC ). It is never a loose file. It lives bundled inside SAVEGAME.sav and inside some module archives. repute.2da is the static definition table the engine reads to build factions for a fresh game, and it is the repute entry you will find in chitin.key / 2da.bif. Go looking for repute.fac on disk and you will not find it. Only repute.2da turns up.

See the Save Game Deep Dive for how the save archive bundles its resources.

Implemented Linter Rules (Rakata-Lint)

None yet. The format is documented here ahead of any dedicated rakata-lint rules.


Every label the schema declares

Generated from the schema, so no label can be quietly left out. How to read these tables.

Fields nobody has examined

Whether the engine reads these has not been established, which is not the same as establishing that it does not. Where When absent carries an answer, that half is settled.

FieldTypeWhen absent
FactionListListNOT EXAMINED; we substitute container
FactionList[].FactionNameCExoStringstamps ""
FactionList[].FactionParentIDDWORDstamps 0
FactionList[].FactionGlobalWORDstamps 1
RepListListnot one constant; we substitute container
RepList[].FactionID1DWORDstamps 0
RepList[].FactionID2DWORDstamps 0
RepList[].FactionRep (required)DWORDstamps 0