GVT Format (Global Variable Table)
The globalvars (GLOBALVARS.res) file is the campaign’s plot state (type GVT ): the flags and counters scripts read and set across the whole playthrough. It sits loose in the save folder, written at save time.
At a Glance
| Property | Value |
|---|---|
| Filename | GLOBALVARS.res |
| Magic Signature | GVT / V3.2 |
| Type | Global Variable Table |
| Rust Reference | Handled by rakata-save (mid-refactor). |
The Four Global Types
Globals come in four types, one for each kind of variable a script can stash between sessions. Each type has its own get/set accessor (the engine’s GetValueBoolean / SetValueBoolean, and the Number, Location, and String variants), so what a type is for is what shapes how it is stored and how many of it the engine keeps room for:
| Type | Holds | Cap | What scripts use it for |
|---|---|---|---|
| Boolean | a single bit | 900 | Plot switches and one-shot guards: has this happened? The most common kind of global. |
| Number | a single unsigned byte (0-255) | 500 | Small counters and quest-stage enumerations. It is a byte, not a 32-bit integer, so it cannot hold an arbitrary count. |
| Location | a position and orientation | 100 | A remembered spot to send, spawn, or move an object to later. |
| String | a short text value | 5 | A handful of named text tokens; scripts rarely need one. |
The caps are hard limits: any identifier past a type’s cap is dropped on load with a “won’t fit” log.
Each type pairs a catalogue list of names with a positional value block: the name of global i is Cat<Type> element i, and its value is position i of the matching Val<Type> block. The catalogue maps each value slot back to its name.
| Catalogue (names) | Value block | Encoding |
|---|---|---|
CatBoolean | ValBoolean | VOID, bit-packed. Boolean i is bit 7 - (i & 7) of byte i >> 3 (most-significant bit first). Block length is (count >> 3) + 1 bytes. |
CatNumber | ValNumber | VOID, one unsigned byte per number. Number i is byte i; values are 0-255. |
CatLocation | ValLocation | VOID, a fixed 2400-byte array of 100 slots of 24 bytes each. Location i is slot i; unused slots are zero, and the block is written whole. |
CatString | ValString | LIST, one struct per string carrying a String (CExoString). |
Each Cat* element is a struct with a Name (CExoString). Because the Val* blocks are positional, dropping or reordering a catalogue entry silently reassigns every later value.
Warning
There are four global types, not two. A model that handles only
CatNumber/CatBooleansilently drops everyLocationandStringglobal. They are simple to miss, but they are real campaign state that has to round-trip.
Each 24-byte location slot is a CScriptLocation: a position Vector followed by an orientation Vector, with no area reference in K1.
| Field | Type | Meaning |
|---|---|---|
Position | Vector (three float32, LE) | The stored point (X, Y, Z), bytes 0x00-0x0b. |
Orientation | Vector (three float32, LE) | The stored facing (X, Y, Z), bytes 0x0c-0x17. |
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 encoding, the per-type accessors, and the location layout are read from:
| Function | Address | Covers |
|---|---|---|
CSWGlobalVariableTable::WriteTable | 0x005299b0 | Value-block encoding on write |
CSWGlobalVariableTable::ReadTableWithCatalogue | 0x0052a280 | Encoding and per-type caps on read |
CSWGlobalVariableTable::GetValueBoolean | 0x00529110 | Boolean value read (the script get) |
CSWGlobalVariableTable::GetValueNumber | 0x00529240 | Number value read |
CSWGlobalVariableTable::GetValueLocation | 0x00529350 | Location value read (slot copy) |
CSWGlobalVariableTable::GetValueString | 0x00529460 | String value read |
CSWSObject::GetScriptLocation | 0x004cb7b0 | Location field order (position then orientation) |
Note
The script layer is not documented yet. These are the engine’s internal per-type accessors. The NCS/NSS-facing script functions (
GetGlobalBoolean/SetGlobalBooleanand theNumber/Location/Stringpairs) that call them are still to be mapped and documented.
Absent-value behaviour
What the engine holds when a label is missing. This is the odd one of the three sidecars: the question isn’t “what does field X default to,” because Boolean/Number/Location values aren’t fields, they’re positions inside VOID blobs, and the catalogue/value pairing means an absence can happen on either half independently. Traced from CSWGlobalVariableTable::ReadTableWithCatalogue (0x0052a280). Provenance: derived, not attested.
Everything is zeroed before anything is read. The very first thing this function does, before touching the file at all, is clear its own destination arrays whole: every boolean bit, every number byte, every location’s position and orientation, and every string slot. That baseline matters below, because several absences don’t substitute a value at all. They just leave this zero state untouched.
A whole-block-absent Val* is a producible, handled state, not a special case. For ValBoolean, ValNumber, and ValLocation, the VOID block is read once with ReadFieldVOID, and the entire catalogue walk for that type (both reading Cat* names and applying Val* values) sits behind a check on whether that read found anything. When it’s absent:
- The catalogue (
CatBoolean/CatNumber/CatLocation) is never even consulted, not just the values. - The destination array for that type keeps the whole-table zero from the top of the function: every boolean reads
false, every number reads0, every location’s position and orientation both read(0, 0, 0).
So “what does the engine hold when ValNumber is absent” does have a clean answer: every number global is 0, exactly as if each one had been explicitly written as 0. From the read side, an absent block is indistinguishable from one present and fully zeroed.
ValString is not a VOID read at all, and follows the ordinary list-absent shape. Per the encoding table above, this one is a GFF list, and it’s gated the way any list is: if GetList on ValString fails, the whole string block (value list and catalogue both) is skipped, and every string slot keeps the top-of-function empty-string baseline. Each list element’s own String field, when the element is present but the field itself isn’t, defaults to "" the same as any other CExoString field traced across this project.
The four caps (900/500/100/5) are enforced once, at catalogue registration, not at the value read. Confirming the note already on this page: each type’s identifier count is checked against its cap (boolean_count < 900, number_count < 500, location_count < 100, string_count < 5) at the point a new name is registered from the catalogue. An identifier past the cap is dropped there, logged, and never registered, so its value, wherever it sits in the parallel Val* block, is never applied to anything. Two independent caps exist for strings specifically: the value list is itself truncated to the first 5 elements before the catalogue walk even starts, and the catalogue registration re-checks the same cap independently.
The pairing is name-driven rather than positional, so the two halves cannot desync. The catalogue is what gets walked, and each entry’s Name selects which slot of the matching Val* block is read and applied. Nothing ever walks a Val* block on its own looking for a name to match it.
So an entry whose Name is absent or empty is an ordinary handled case rather than a corruption: the read finds "", the code tests for exactly that before doing anything, and an empty name skips registration. That catalogue position contributes nothing, and whatever sat in the corresponding Val* slot is never surfaced as a global.
Which means a half-present pair is not a state the engine can be handed. It never looks for the second half except by walking the name that identifies it.
Implemented Linter Rules (Rakata-Lint)
None yet. Documented here ahead of any dedicated rakata-lint rules.