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

GIT Format (Game Instance Template)

Description: The Game Instance Template (.git) orchestrates the exact placement of every single entity within an environment. If the .are file is the underlying “stage”, the .git file acts as the blueprint for its “actors”–defining exactly where creatures initially spawn, where placeables sit, the physical rotation of doors, and the bounds of any active sound emitters.

At a Glance

PropertyValue
Extension(s).git
Magic SignatureGIT / V3.2
TypeInstance Blueprint
Rust ReferenceView rakata_generics::Git in Rustdocs

Data Model Structure

Rakata maps a Game Instance Template into the rakata_generics::Git struct. The struct’s Rustdocs document every field’s binary schema and GFF mapping; the table below is the high-level anatomy.

CategoryCoversRepresentative fields
Root BehaviorTemplate-versus-inline loading mode and the live weather stateUseTemplates, CurrentWeather, WeatherStarted
Object Instance ListsOne list per entity class, placing creatures, doors, placeables, triggers, sounds, encounters, waypoints, stores, items, cameras, and area effectsCreature List, Door List, TriggerList, SoundList
Per-Instance PlacementEach element’s template reference, position, and orientation (field naming varies by entity class; see below)TemplateResRef, XPosition, Bearing, ObjectId
Saved SnapshotsThe full inline object each list holds instead when UseTemplates = 0, as a savegame GIT stores itSavedCreature, SavedDoor, SavedPlaceable, SavedTrigger
Area SingletonsThe stealth and ambient-audio state struct, plus the save-only minimap exploration blobAreaProperties, AreaMap

rakata-lint validates these fields against the engine constraints documented below.

Engine Audits & Decompilation

The following documents the engine’s exact load sequence and field requirements for .git files mapped from swkotor.exe.

(Decompilation logic for this section was entirely audited and verified via native Ghidra pipeline against swkotor.exe, explicitly pulling from CSWSArea::LoadGIT at 0x0050dd80.)

The LoadGIT subroutine is a massive dispatcher. It evaluates 3 immediate root scalars before handing off evaluation to 13 distinct object-list loaders mapping entities. Crucially, the flag UseTemplates dominates this process by dictating whether these lists refer to external files or contain fully inline entity data.

Root Behavior Properties

FieldTypeEngine Evaluation
UseTemplatesBYTEControls whether object arrays read TemplateResRef to construct entities, or fall back to inline evaluation.
CurrentWeatherBYTEStandard BYTE. Zeroed to 0xFF on Interior Areas. Absent-field default (before that interior override runs) is a fresh literal 0, unconditional – it overwrites whatever the object held, not a carry-over.
WeatherStartedBYTEStandard BYTE. Zeroed to 0 on Interior Areas. Same mechanism as CurrentWeather: absent-field default is a fresh literal 0, unconditional, applied before the interior override.

(The engine validates weather fields against the .are properties immediately during load).

Notably, UseTemplates itself has no save-writer path at all: SaveGIT never emits this field, confirmed by inspecting a real save file’s GIT struct directly (no UseTemplates field present). Every save relies entirely on the loader’s own hardcoded default of 0.

Note

The same schema serves two roles, and UseTemplates is the discriminator. A module’s static .git sets UseTemplates = 1: each object element is a sparse placement that carries a TemplateResRef, and the engine loads the matching blueprint (.utc/.utd/.utp/.utt/…) and overlays the few instance fields the element holds. A savegame GIT (bundled inside SAVEGAME.sav) instead sets UseTemplates = 0: each element is a full self-contained snapshot read field by field, with no TemplateResRef and no blueprint load. A field missing from a UseTemplates = 0 element resolves to the engine’s hardcoded default, not to a blueprint. See the Save Game Deep Dive for how savegames bundle and read these GITs.

One object type is effectively savegame-only: area-of-effect objects (AreaEffectList) are runtime spell/ability effects with no template (note their absence from the GIT-006 TemplateResRef set below), so they appear in savegame GITs rather than static module layouts.

How Rakata Models the Two Forms

Each dispatching list is a GitObjects<Static, Saved>: Static for UseTemplates = 1 placements, Saved for UseTemplates = 0 snapshots. The flag is not kept as a separate field on Git, because the enum already records the choice and storing both would let them disagree.

ListStatic formSaved form
Creature ListGitCreatureSavedCreature
Door ListGitDoorSavedDoor
Placeable ListGitPlaceableSavedPlaceable
TriggerListGitTriggerSavedTrigger
StoreListGitStoreSavedStore
SoundListGitSoundSavedSound
List (items)GitItemSavedItem

Three lists sit outside that shape:

  • WaypointList is a plain Vec<GitWaypoint>, because LoadWaypoints ignores the flag entirely and there is only ever one form to read.
  • AreaEffectList is a plain Vec<GitAreaEffect>, because these objects have no blueprint at all, so the saved form is the only form.
  • Encounter List is still read as static placements whatever the flag says, which is wrong for a savegame GIT. It is the one list whose saved field set is not modelled: no save in the fixture corpus carries an encounter, and unlike area effects its saved layout is not written down here either, so there would be nothing to check a model against.

Saved snapshots are checked against the fixture save corpus rather than synthetic fixtures: every object of every modelled type is parsed, written back, and compared field by field against what the engine originally wrote. AreaEffectList is the exception, since no fixture save has one; its field set comes from the load and save behaviour documented below, and a test asserts the list is still empty so a corpus that grows one does not go unchecked.

Field Naming Inconsistencies

Due to legacy asset sprawl, the engine evaluates vectors explicitly according to vastly different naming conventions depending entirely on the entity class. This is hardcoded into swkotor.exe.

Target ListsPosition ParadigmOrientation Paradigm
Creatures, Triggers, Items, Waypoints, StoresXPosition, YPosition, ZPositionXOrientation, YOrientation, ZOrientation (vector)
Doors, PlaceablesX, Y, ZBearing (single float angle)
Area EffectsPositionX, PositionY, PositionZOrientationX, OrientationY, OrientationZ (vector)
Sounds, EncountersXPosition, YPosition, ZPosition(none at the object level)

Bearing does not mean the same thing for the two classes that use it: a door stores its scalar bearing verbatim, while a placeable derives Bearing from the yaw of its orientation at save time (lossy). Encounter spawn points (inside SpawnPointList) carry a single-float Orientation per point, distinct from both the Bearing scalar and the orientation vector. Sounds have no orientation at all; they use Positional / RandomPosition flags plus RandomRangeX / RandomRangeY instead.

Warning

Orientation Normalization The engine strictly evaluates 3D orientation logic. If a normalized orientation vector (like in StoreList or AreaEffectList) inadvertently resolves to 0.0 unconditionally, the engine catches the math fault and applies a hard fallback vector to (0, 1, 0).

Standard Instance Arrays

Standard loaders evaluate the generic ObjectId, process the localized position/orientation floats, and dispatch behavior mapping logic.

List NameStruct TargetEngine Triggers & Fallbacks
Creature ListLoadCreaturesPositions are explicitly validated defensively through ComputeSafeLocation bounds.
Door ListLoadDoorsSave states trigger LoadObjectState. External templates dynamically route to LoadDoorExternal.
WaypointListLoadWaypointsCompletely ignores UseTemplates–it solely relies on inline data! Z-height is shifted dynamically via ComputeHeight.
TriggerListLoadTriggersGeometry properties reuse native UTT formatting. Contains unique linkage arrays: LinkedToModule, TransitionDestination, LinkedTo.

ObjectId Has One Default Across Every List, Static or Saved

ObjectId isn’t read by any of the per-type field loaders (LoadDoor, LoadPlaceable, LoadTrigger, the sound/store/encounter/item loaders, LoadWaypoint). It’s read exactly once per element, by the area-level list dispatcher (CSWSArea::LoadDoors, LoadPlaceables, LoadTriggers, LoadSounds, LoadStores, LoadEncounters, LoadItems, LoadWaypoints, LoadAreaEffects), before that dispatcher branches into the static-template path or the full-instance path. Every one of these nine dispatchers uses the identical default: 0x7F000000 (OBJECT_INVALID). Since the read happens once, ahead of the branch, and both branches receive the same already-assigned id, there is no static-versus-saved distinction for this field anywhere in the engine – one default covers every list, in both forms.

Creature is a structural exception, and on the static path the field is dead, not defaulted. CSWSArea::LoadCreatures branches on UseTemplates before touching ObjectId, not after. The full/save branch reads ObjectId the same way as every other type – CResGFF::ReadFieldDWORD(..., "ObjectId", ..., 0x7F000000) – so that branch genuinely defaults an absent field, same as everywhere else. The static/template branch is different in kind, not just mechanism: confirmed by decompilation, it never issues a ReadField* call against "ObjectId" at all, anywhere in the branch. 0x7F000000 reaches the CSWSCreature constructor as a bare literal, not a value derived from any read. Both branches land on the same number, but only one of them is “defaulting an absent field” – the static branch never looks at the field to begin with. Per this codebase’s modeling rule (model a field only where the engine reads it at that path), ObjectId is genuinely dead on a static creature placement, not merely defaulted: whatever a static .git creature entry carries there has zero effect, the same “never looked up” status already established for other confirmed-dead fields like .utc’s Tail/Wings. This is specific to Creature – the other nine dispatchers read ObjectId once, ahead of their own static/save branch, so the field is genuinely read-with-default on both of their paths.

One corollary worth flagging for rakata’s own code: the field-level default documented for AreaEffectList below isn’t a special case, it’s the general rule – and rakata’s typed views currently only apply OBJECT_INVALID to the ten static (UseTemplates = 1) types. The seven Saved* types (the UseTemplates = 0 full-snapshot forms) currently default ObjectId to plain 0 in code, which this trace shows is wrong: the full/save branch uses the identical 0x7F000000-default read as the static branch, for every type. module.ifo’s Mod_Area_list has the same gap – its ObjectId read (gated behind the save-game flag, confirming the existing “only read inside a save state flow” note) also defaults to 0x7F000000, not the 0 currently documented and coded there.

Appearance Is a Real Instance Override on Doors and Placeables

Both CSWSDoor::LoadDoor and CSWSPlaceable::LoadPlaceable read a field literally named Appearance as a DWORD and truncate it to a single byte – the same truncation pattern already documented for the .utd/.utp blueprint reads. For doors, the resolved byte immediately keys doortypes.2da’s Model/VisibleModel columns to pick the door’s mesh, so this is a genuine “which visual model represents this object” field, not inert data, and it shares that meaning with the blueprint-level Appearance documented on UTD/UTP. It defaults to 0 when absent (a hardcoded literal on the placeable path; the door struct’s own zero-initialized member on the door path).

Appearance is already modeled on rakata’s SavedDoor/SavedPlaceable types (the UseTemplates = 0 save-snapshot form). It is not currently modeled on the sparse GitDoor/GitPlaceable types (UseTemplates = 1, template placements), which only carry Bearing/position/ObjectId. LoadDoor/LoadPlaceable are the same shared field readers regardless of which path calls them, so a sparse, template-referencing door or placeable placement that also carries its own Appearance value would have that value read as an override on top of whatever the referenced blueprint supplies – worth verifying against real static (UseTemplates = 1) .git entries before deciding whether GitDoor/GitPlaceable need the field added.

Compare UTE’s Appearance, which shares the label but is never read by the engine at all – confirming the two are unrelated despite the shared name. A placed WaypointList[].Appearance follows the same dead pattern as UTE’s, not Door/Placeable’s: LoadWaypoint’s fully-decompiled field list has no room for it, confirmed directly (see UTW’s Core Structural Findings). A waypoint has no rendered model to select in the first place, so this is the expected outcome, not a surprising one.

Description in a GIT Struct Is Toolset Residue, Not an Override

Description is the opposite case from Appearance: it’s a real, blueprint-level field on both UTD and UTP, but the area loader never reads it back out of a placed instance’s own GIT entry. Traced through both CSWSDoor::LoadDoorExternal and CSWSArea::LoadPlaceables: the ordinary GIT-instance path resolves Description (and every other non-instance field) entirely from the referenced .utd/.utp blueprint via LoadFromTemplate, and the four-field door overlay (TransitionDestin/LinkedTo/LinkedToFlags/LinkedToModule, see UTD) doesn’t include Description. A Description value sitting in a GIT file’s own Door or Placeable entry is written by the toolset but never consulted at load time – consistent with the corpus finding that 99 files carry the field and only one holds a non-empty value. It also isn’t area-level metadata: CSWSArea::LoadProperties, the function that reads the GIT’s own AreaProperties struct, has no Description field at all.

This was confirmed for Door and Placeable entries specifically; Creature, Item/Store, Trigger, Encounter, Sound, and Camera entries weren’t individually checked, so treat the same conclusion as likely but unproven for those list types. Waypoints are a related but distinct case, not just an unchecked one – see UTW’s Core Structural Findings, where a placed waypoint’s Description isn’t toolset residue with a source that goes unread, it has no source at all, because waypoints never resolve a TemplateResRef in the first place.

AreaProperties.EnvAudio Is Toolset-Only Duplication

The GIT’s own AreaProperties struct carries an EnvAudio INT in most vanilla files (present with a real value in 99 of 117 GIT files in a full install), but it’s a different field entirely from ARE’s per-room EnvAudio – same name, different struct, and this one has no engine consumer at all. CSWSArea::LoadProperties (the function that reads GIT’s AreaProperties) and its ambient-sound delegate, CSWSAmbientSound::Load (which reads the already-modeled MusicDelay/MusicDay/MusicNight/MusicBattle/AmbientSndDay/AmbientSndNight/AmbientSndDayVol/AmbientSndNitVol off that same struct), read neither field named EnvAudio between them. A binary-wide check confirms this isn’t a missed function: the "EnvAudio" string exists exactly once in the whole executable, with exactly one cross-reference, and that one reference is the ARE per-room reader, not anything reachable from AreaProperties.

So a GIT-level EnvAudio value has zero engine consumers, full stop. It’s plausibly a toolset habit – the area’s default or primary room EnvAudio value duplicated onto AreaProperties by the editor UI, never kept in sync with the real per-room values and never read back by the engine – but that’s the strongest claim the evidence supports, not a confirmed mechanism. There is no absent-field default to report, since nothing ever looks for the field in the first place.

The Blueprint’s Tag Always Wins

UTD’s “Save versus Template Load Paths” documents that a templated door’s Tag comes from its .utd blueprint, with no per-instance overlay – so two doors sharing one blueprint would share one tag. That turns out to be general engine behavior, not a door-specific gap: every templated GIT object type follows the identical pattern. Confirmed directly for Placeable, Trigger, Sound, Store, and Encounter, and for Creature and Item as time allowed:

  • Placeable, Sound, Encounter, Creature, Item: the shared field-reading routine (LoadPlaceable, CSWSSoundObject::Load, ReadEncounterFromGff, the LoadCreature/LoadFromTemplate pair via ReadStatsFromGff, LoadDataFromGff) reads Tag unconditionally from whichever struct it’s handed – the placed instance’s own struct on a direct load, the blueprint’s top-level struct on a template load. The area-level dispatcher’s post-template overlay, where one exists at all, only ever covers position/orientation/geometry – never Tag.
  • Trigger: same pattern, and Tag sits in the identical family as the door overlay – CSWSArea::LoadTriggers overlays TransitionDestination/LinkedTo/LinkedToModule/LinkedToFlags plus position/geometry back from the GIT instance after a template load, but Tag is conspicuously not among them.
  • Store: uses ResRef rather than TemplateResRef for templating (already documented elsewhere on this page), but does carry a genuine, separate Tag field, read unconditionally by LoadStore the same way. LoadStores’ post-template overlay covers only orientation and position.

So there’s no engine-side protection anywhere against two placements that share one blueprint ending up with the same Tag – for any templated object type, not just doors. Vanilla avoids the collision purely by authoring convention (effectively one blueprint per placed instance), the same workaround already documented for doors. A lint rule for this should be written generically across every templating GIT object type, not scoped to doors.

Specialized Struct Parsings

Engine Dispatch TargetDescription & Findings
LoadSounds (0x00505560)Discard logic: Translates GeneratedType via DWord, but physically truncates it to an 8-bit byte on save, silently discarding the upper 24 bits!
LoadEncounters (0x00505060)Highly nested structural array reusing both Geometry and SpawnPointList formats natively built for UTE boundaries.
LoadPlaceableCameras (0x00505eb0)Client-side only struct that reads composite GFF spatial types correctly natively! Camera Limit: If it hits 51 camera entries, the loader formally rejects it.
“List” (Items) (0x00504de0)Bizarrely, the generic parent entity list List is used specifically to orchestrate Item instances!

Area-of-Effect Objects (Save-Only)

AreaEffectList holds runtime spell/ability effect objects (CSWSAreaOfEffectObject). As noted above, these have no blueprint file of their own – no loader anywhere in the binary ever opens a template for one, so every field is read straight off the GIT struct itself. Elements must carry struct type id 13 or the loader skips them; ObjectId defaults to the engine-wide 0x7F000000 (OBJECT_INVALID) placeholder – see above for why this is the general rule across every list, not a special case for area effects.

FieldTypeDefaultEngine Evaluation
TagCExoString""
AreaEffectIdINT0A freshly constructed object leaves this member uninitialized; the constant load default masks that gap.
SpellIdDWORD0A fresh object uses an internal 0xFFFFFFFF sentinel, and the writer emits the value through an accessor rather than the raw member – but a field genuinely missing from a save still resolves to 0 on load.
ShapeBYTE00 = circle, 1 = rectangle. Any other value skips both dimension fields entirely, so the effect gets no shape geometry at all.
MetaMagicTypeBYTE0
SpellSaveDCINT0Fresh objects start at 14; the 0 default only applies when a save’s GFF genuinely omits the field.
SpellLevelINT0
RadiusFLOAT0.0Only read/written when Shape == 0.
Length / WidthFLOAT0.0 eachOnly read/written when Shape == 1; the pair round-trips symmetrically (the same members map to the same labels on both load and save).
CreatorId / LinkedToObject / LastEntered / LastLeftDWORD0 eachFresh objects use the 0x7F000000 placeholder instead; 0 is only the fallback for a field genuinely absent from the save.
DurationDWORD0
DurationTypeBYTE0Fresh objects start at 2.
LastHrtbtDay / LastHrtbtTimeDWORD0 each
PositionX / PositionY / PositionZFLOAT0.0 eachRead last, by the area-effect list loader, and passed straight into placement.
OrientationX / OrientationY / OrientationZFLOAT0.0 eachNormalized on load; if the vector’s squared magnitude is negligibly small (<= 0.0001) it falls back to (0, 1, 0).

Note

Write-only script fields, confirmed dead on restore. OnHeartbeat, OnUserDefined, OnObjEnter, and OnObjExit are written by the save writer but never read back by any loader in the binary. A restored area-of-effect object genuinely never fires these four events again: nothing re-derives them from AreaEffectId or SpellId on the load path. That 2DA-driven derivation does exist (vfx_persistent.2da, keyed by AreaEffectId, supplying OnHeartbeat/OnObjEnter/OnObjExit script resrefs plus shape/size defaults), but it’s wired exclusively into the fresh spell-cast creation path, not into loading a save. The GIT loader has no equivalent “look this back up” step. OnUserDefined goes further still: it’s never populated by any path, including fresh creation, so it’s effectively dead in this engine build regardless of load versus save.

This doesn’t mean a restored effect goes inert, though: duration countdown, position tracking, and collision-based area membership all keep working after a reload, since those run off the object’s own stored data (Duration, DurationType, position) rather than off scripts. It’s specifically the four scripted event hooks that go silent.

Singular Structs

  • AreaProperties: Orchestrates stealth behavior state tracking and dynamic audio states. It physically reads AmbientSndDayVol / AmbientSndNitVol and explicitly truncates their INT declarations into a single native runtime byte value. The loader and the save writer disagree on where several of these fields actually live: the writer nests RestrictMode, StealthXPMax, StealthXPCurrent, StealthXPLoss, StealthXPEnabled, and SunFogColor inside the AreaProperties struct, but the reader actually pulls those specific fields from the GIT’s top level instead – only Unescapable is genuinely read from inside AreaProperties. The practical effect: those six fields are permanently dead in every save this engine’s own writer produces. They’re always written to a location the reader never checks, so they always resolve to whatever value the object already had in memory. TransPending / TransPendNextID / TransPendCurrID are written in both places (the GIT top level directly, and a redundant copy inside AreaProperties), but only the top-level copy is ever consulted, so the AreaProperties copy is a harmless duplicate. This finding is grounded directly in decompiled code with reasonably high confidence, though it’s inferred from variable-usage patterns rather than a byte-level disassembly proof. Its MusicDelay/MusicDay/MusicNight/MusicBattle/AmbientSndDay/AmbientSndNight/AmbientSndDayVol/AmbientSndNitVol fields (read by the ambient-sound delegate, CSWSAmbientSound::Load) each carry over their constructor’s pre-armed value when absent – confirmed as a genuine carry-over with no divergence between the constructed value and the read’s own fallback, unlike the constructor/literal-default mismatches found elsewhere in this codebase’s format audits. Absent defaults: MusicDelay 5000, MusicDay 2, MusicNight 3, MusicBattle 1, AmbientSndDay 1, AmbientSndNight 2, AmbientSndDayVol/AmbientSndNitVol 0 each.
  • AreaMap: Strict binary blobs evaluating rendering properties (AreaMapData). It is absolutely bypassed during fresh loads, only executed conditionally during save-game states.
  • CameraList (GitCamera entries): Read by LoadPlaceableCameras (already documented above for its 51-entry rejection limit). Position defaults to the literal zero vector (0, 0, 0) and Orientation to the literal identity quaternion (w=1, x=0, y=0, z=0), both unconditional – the read helpers always populate the field from either the GFF data or the supplied default, and the loader never branches on which one it got. Absence of either field doesn’t drop the camera entry; it’s always kept and registered, with each field defaulting independently. No presence-chain abort exists in this loop beyond the already-documented 51-entry count cap.

Implemented Linter Rules (Rakata-Lint)

Phase 1 (intra-resource, no context)

Implemented under rakata_lint::rules::git.

  1. GIT-001 (Weather Zeroing): Informs when CurrentWeather != 0xFF or WeatherStarted=true is configured; if the area is an interior, the engine forcibly zeros these on load.
  2. GIT-002 (Camera Array Bounds): Errors when CameraList contains 51 or more entries; triggers an immediate engine-level loader failure.
  3. GIT-003 (Stealth Clamping): Warns when StealthXPCurrent > StealthXPMax; the engine clamps on evaluation.
  4. GIT-004 (Ambient Volume Truncation): Warns when AmbientSndDayVol or AmbientSndNitVol are outside 0..=255; the engine truncates to an 8-bit byte.
  5. GIT-005 (Sound GeneratedType Truncation): Warns when any sound’s GeneratedType exceeds 255; the engine truncates to an 8-bit byte on save. Both forms of SoundList carry the field, so this one applies to savegame content as well as static.

Phase 2 (resource existence, requires LintContext)

Implemented under rakata_lint::rules::git_range.

  1. GIT-006 (Template Resref Existence): Warns when any per-instance TemplateResRef does not resolve to its expected typed template file: CreatureList[].TemplateResRef (.utc), List[].TemplateResRef (.uti, item instances), Door List[].TemplateResRef (.utd), Placeable List[].TemplateResRef (.utp), SoundList[].TemplateResRef (.uts), TriggerList[].TemplateResRef (.utt), StoreList[].ResRef (.utm), and Encounter List[].TemplateResRef (.ute). Waypoint instances are genuinely inlined and have no template – confirmed by decompilation, LoadWaypoint never reads a field named TemplateResRef under any circumstance. Doors are not, and this rule excluded them on that assumption until the gap was found: a static (UseTemplates = 1) door placement resolves TemplateResRef against a .utd blueprint exactly like a creature or placeable does (see UTD’s “Save versus Template Load Paths”). GitDoor now carries the field and the rule checks it, so a door pointing at a missing blueprint no longer passes clean. Trigger LinkedToModule is deferred to Phase 3 cross-resource checks. The rule only looks at the static form of each list, since a UseTemplates = 0 snapshot has no resref to resolve in the first place.