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
| Property | Value |
|---|---|
| Extension(s) | .git |
| Magic Signature | GIT / V3.2 |
| Type | Instance Blueprint |
| Rust Reference | View 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.
| Category | Covers | Representative fields |
|---|---|---|
| Root Behavior | Template-versus-inline loading mode and the live weather state | UseTemplates, CurrentWeather, WeatherStarted |
| Object Instance Lists | One list per entity class, placing creatures, doors, placeables, triggers, sounds, encounters, waypoints, stores, items, cameras, and area effects | Creature List, Door List, TriggerList, SoundList |
| Per-Instance Placement | Each element’s template reference, position, and orientation (field naming varies by entity class; see below) | TemplateResRef, XPosition, Bearing, ObjectId |
| Saved Snapshots | The full inline object each list holds instead when UseTemplates = 0, as a savegame GIT stores it | SavedCreature, SavedDoor, SavedPlaceable, SavedTrigger |
| Area Singletons | The stealth and ambient-audio state struct, plus the save-only minimap exploration blob | AreaProperties, 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
| Field | Type | Engine Evaluation |
|---|---|---|
UseTemplates | BYTE | Controls whether object arrays read TemplateResRef to construct entities, or fall back to inline evaluation. |
CurrentWeather | BYTE | Standard 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. |
WeatherStarted | BYTE | Standard 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
UseTemplatesis the discriminator. A module’s static.gitsetsUseTemplates = 1: each object element is a sparse placement that carries aTemplateResRef, and the engine loads the matching blueprint (.utc/.utd/.utp/.utt/…) and overlays the few instance fields the element holds. A savegame GIT (bundled insideSAVEGAME.sav) instead setsUseTemplates = 0: each element is a full self-contained snapshot read field by field, with noTemplateResRefand no blueprint load. A field missing from aUseTemplates = 0element 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-006TemplateResRefset 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.
| List | Static form | Saved form |
|---|---|---|
Creature List | GitCreature | SavedCreature |
Door List | GitDoor | SavedDoor |
Placeable List | GitPlaceable | SavedPlaceable |
TriggerList | GitTrigger | SavedTrigger |
StoreList | GitStore | SavedStore |
SoundList | GitSound | SavedSound |
List (items) | GitItem | SavedItem |
Three lists sit outside that shape:
WaypointListis a plainVec<GitWaypoint>, becauseLoadWaypointsignores the flag entirely and there is only ever one form to read.AreaEffectListis a plainVec<GitAreaEffect>, because these objects have no blueprint at all, so the saved form is the only form.Encounter Listis 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 Lists | Position Paradigm | Orientation Paradigm |
|---|---|---|
| Creatures, Triggers, Items, Waypoints, Stores | XPosition, YPosition, ZPosition | XOrientation, YOrientation, ZOrientation (vector) |
| Doors, Placeables | X, Y, Z | Bearing (single float angle) |
| Area Effects | PositionX, PositionY, PositionZ | OrientationX, OrientationY, OrientationZ (vector) |
| Sounds, Encounters | XPosition, 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
StoreListorAreaEffectList) inadvertently resolves to0.0unconditionally, 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 Name | Struct Target | Engine Triggers & Fallbacks |
|---|---|---|
| Creature List | LoadCreatures | Positions are explicitly validated defensively through ComputeSafeLocation bounds. |
| Door List | LoadDoors | Save states trigger LoadObjectState. External templates dynamically route to LoadDoorExternal. |
| WaypointList | LoadWaypoints | Completely ignores UseTemplates–it solely relies on inline data! Z-height is shifted dynamically via ComputeHeight. |
| TriggerList | LoadTriggers | Geometry 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, theLoadCreature/LoadFromTemplatepair viaReadStatsFromGff,LoadDataFromGff) readsTagunconditionally 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 – neverTag. - Trigger: same pattern, and
Tagsits in the identical family as the door overlay –CSWSArea::LoadTriggersoverlaysTransitionDestination/LinkedTo/LinkedToModule/LinkedToFlagsplus position/geometry back from the GIT instance after a template load, butTagis conspicuously not among them. - Store: uses
ResRefrather thanTemplateResReffor templating (already documented elsewhere on this page), but does carry a genuine, separateTagfield, read unconditionally byLoadStorethe 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 Target | Description & 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.
| Field | Type | Default | Engine Evaluation |
|---|---|---|---|
Tag | CExoString | "" | |
AreaEffectId | INT | 0 | A freshly constructed object leaves this member uninitialized; the constant load default masks that gap. |
SpellId | DWORD | 0 | A 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. |
Shape | BYTE | 0 | 0 = circle, 1 = rectangle. Any other value skips both dimension fields entirely, so the effect gets no shape geometry at all. |
MetaMagicType | BYTE | 0 | |
SpellSaveDC | INT | 0 | Fresh objects start at 14; the 0 default only applies when a save’s GFF genuinely omits the field. |
SpellLevel | INT | 0 | |
Radius | FLOAT | 0.0 | Only read/written when Shape == 0. |
Length / Width | FLOAT | 0.0 each | Only 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 / LastLeft | DWORD | 0 each | Fresh objects use the 0x7F000000 placeholder instead; 0 is only the fallback for a field genuinely absent from the save. |
Duration | DWORD | 0 | |
DurationType | BYTE | 0 | Fresh objects start at 2. |
LastHrtbtDay / LastHrtbtTime | DWORD | 0 each | |
PositionX / PositionY / PositionZ | FLOAT | 0.0 each | Read last, by the area-effect list loader, and passed straight into placement. |
OrientationX / OrientationY / OrientationZ | FLOAT | 0.0 each | Normalized 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, andOnObjExitare 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 fromAreaEffectIdorSpellIdon the load path. That 2DA-driven derivation does exist (vfx_persistent.2da, keyed byAreaEffectId, supplyingOnHeartbeat/OnObjEnter/OnObjExitscript 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.OnUserDefinedgoes 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/AmbientSndNitVoland explicitly truncates theirINTdeclarations into a single native runtime byte value. The loader and the save writer disagree on where several of these fields actually live: the writer nestsRestrictMode,StealthXPMax,StealthXPCurrent,StealthXPLoss,StealthXPEnabled, andSunFogColorinside theAreaPropertiesstruct, but the reader actually pulls those specific fields from the GIT’s top level instead – onlyUnescapableis genuinely read from insideAreaProperties. 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/TransPendCurrIDare written in both places (the GIT top level directly, and a redundant copy insideAreaProperties), but only the top-level copy is ever consulted, so theAreaPropertiescopy 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. ItsMusicDelay/MusicDay/MusicNight/MusicBattle/AmbientSndDay/AmbientSndNight/AmbientSndDayVol/AmbientSndNitVolfields (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:MusicDelay5000,MusicDay2,MusicNight3,MusicBattle1,AmbientSndDay1,AmbientSndNight2,AmbientSndDayVol/AmbientSndNitVol0each. - AreaMap: Strict binary blobs evaluating rendering properties (
AreaMapData). It is absolutely bypassed during fresh loads, only executed conditionally during save-game states. CameraList(GitCameraentries): Read byLoadPlaceableCameras(already documented above for its 51-entry rejection limit).Positiondefaults to the literal zero vector(0, 0, 0)andOrientationto 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.
- GIT-001 (Weather Zeroing): Informs when
CurrentWeather != 0xFForWeatherStarted=trueis configured; if the area is an interior, the engine forcibly zeros these on load. - GIT-002 (Camera Array Bounds): Errors when
CameraListcontains 51 or more entries; triggers an immediate engine-level loader failure. - GIT-003 (Stealth Clamping): Warns when
StealthXPCurrent > StealthXPMax; the engine clamps on evaluation. - GIT-004 (Ambient Volume Truncation): Warns when
AmbientSndDayVolorAmbientSndNitVolare outside0..=255; the engine truncates to an 8-bit byte. - GIT-005 (Sound GeneratedType Truncation): Warns when any sound’s
GeneratedTypeexceeds 255; the engine truncates to an 8-bit byte on save. Both forms ofSoundListcarry 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.
- GIT-006 (Template Resref Existence): Warns when any per-instance
TemplateResRefdoes 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), andEncounter List[].TemplateResRef(.ute). Waypoint instances are genuinely inlined and have no template – confirmed by decompilation,LoadWaypointnever reads a field namedTemplateResRefunder any circumstance. Doors are not, and this rule excluded them on that assumption until the gap was found: a static (UseTemplates = 1) door placement resolvesTemplateResRefagainst a.utdblueprint exactly like a creature or placeable does (see UTD’s “Save versus Template Load Paths”).GitDoornow carries the field and the rule checks it, so a door pointing at a missing blueprint no longer passes clean. TriggerLinkedToModuleis deferred to Phase 3 cross-resource checks. The rule only looks at the static form of each list, since aUseTemplates = 0snapshot has no resref to resolve in the first place.