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

SSF (Sound Set File)

Sound sets map specific generic triggers (e.g. “Battle Cry”, “Agony”, “Selected”) to physical sound references by mapping enum hooks to strings.

At a Glance

PropertyValue
Extension(s).ssf
Magic SignatureSSF / V1.1
TypeEnum-String Mapping
Rust ReferenceView rakata_formats::Ssf in Rustdocs

File Layout

The smallest container in the set: a 12-byte header and a table of 32-bit StrRef values, with the table located by an offset rather than assumed to follow the header.

BlockSizeLocated by
Header12 bytesAlways at 0x00
Sound table4 bytes per slot, 28 slotssound_table_offset, in practice always 12
Trailing reserved entries4 bytes eachWhatever follows the 28 slots

Header (12 bytes)

OffsetFieldTypeNotes
0x00magicfourccSSF , trailing space included.
0x04versionfourccV1.1.
0x08sound_table_offsetu32Distance to the table. Always 12 in practice, meaning the table abuts the header.

Each slot is a single i32 holding a TLK string reference, and the slot’s position is its meaning. An unset slot carries -1 (0xFFFFFFFF) rather than 0, which matters because 0 is a perfectly valid StrRef.

The 28 slots

Array index on the left, since that is what the file uses. Scripts address these 1-indexed, so a script firing event 1 reads array index 0; the engine subtracts one on the way in.

#Trigger#Trigger#Trigger#Trigger
0Battle cry 17Select 214Low health21Begin search
1Battle cry 28Select 315Dead22Begin unlock
2Battle cry 39Attack grunt 116Critical hit23Unlock failed
3Battle cry 410Attack grunt 217Target immune24Unlock success
4Battle cry 511Attack grunt 318Lay mine25Separated from party
5Battle cry 612Pain grunt 119Disarm mine26Rejoined party
6Select 113Pain grunt 220Begin stealth27Poisoned

Warning

The trailing count is not uniform, and Rakata currently normalises it Real files carry extra -1 values after the 28 slots, and the count varies: most carry 12 trailing entries and a minority carry 21, giving 172-byte and 208-byte files respectively. Rakata emits twelve regardless, a figure inherited from the PyKotor writer rather than measured, so round-tripping one of the larger ones rewrites it into a shape it does not have. A writer should preserve the count it read.

The engine does not care either way, and that is now traced rather than assumed. CSoundSet::GetStrres (0x00678820) bounds its lookup at index != 0 && index < 29, so only slot indices 1 through 28 are ever dereferenced, exactly the documented slots and nothing past them. It resolves the slot address by reading sound_table_offset live out of the loaded buffer rather than assuming a fixed position. Its only caller, GetSoundSetStrres (0x0060b8a0), and PlaySoundSetSound (0x00611470) add no further indexing or length check.

The load and unload hooks parse nothing at all: CResSSF::OnResourceServiced (0x006db690) checks the data pointer is non-null and flips a flag, reading no header field past offset zero. Nothing anywhere in the traced call graph treats a 12-entry file differently from a 21-entry one, so the trailing bytes are inert to this build regardless of which count a file carries.

That makes preserving the count purely a round-trip-fidelity choice on our side, with no engine behaviour riding on it. Why two counts exist at all is not traced, and no struct in the program’s type database models the file beyond the 28 slots.

Engine Audits & Decompilation

Read from CSoundSet::GetStrres at 0x00678820 in swkotor.exe. Provenance: derived, not attested unless a claim says otherwise: the rows have not been separately re-derived, so they sit on the reverse-engineering queue. Individual claims below may carry a level of their own, and where one does it overrides this line for that claim.

Pipeline EventGhidra Provenance & Engine Behavior
Finding the TableThe parser reads a single 4-byte integer (DWORD) at offset +0x08. This number acts as a direct distance pointer, telling the game explicitly where the audio mapping table begins inside the file payload.
Reading the SlotsStarting directly at that pointer, the engine grabs exactly 28 continuous integers. Each position in this span represents a hardcoded character action (e.g. slot 1 is always ‘Battle Cry’, slot 2 is always ‘Agony’).
Handling BlanksObviously, not all characters have recorded audio for every obscure trigger. If a sound slot is supposed to be empty, it utilizes the default sentinel value 0xFFFFFFFF (-1) to let the engine know to skip playback.

Note

1-Indexed Triggers When modders fire off audio events using gameplay scripts, the event identifiers are natively 1-indexed (1 to 28). To find the matching audio string underneath, the engine simply subtracts 1 behind the scenes to correctly navigate the literal 0-indexed array in memory.