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
| Property | Value |
|---|---|
| Extension(s) | .ssf |
| Magic Signature | SSF / V1.1 |
| Type | Enum-String Mapping |
| Rust Reference | View 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.
| Block | Size | Located by |
|---|---|---|
| Header | 12 bytes | Always at 0x00 |
| Sound table | 4 bytes per slot, 28 slots | sound_table_offset, in practice always 12 |
| Trailing reserved entries | 4 bytes each | Whatever follows the 28 slots |
Header (12 bytes)
| Offset | Field | Type | Notes |
|---|---|---|---|
0x00 | magic | fourcc | SSF , trailing space included. |
0x04 | version | fourcc | V1.1. |
0x08 | sound_table_offset | u32 | Distance 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 |
|---|---|---|---|---|---|---|---|
| 0 | Battle cry 1 | 7 | Select 2 | 14 | Low health | 21 | Begin search |
| 1 | Battle cry 2 | 8 | Select 3 | 15 | Dead | 22 | Begin unlock |
| 2 | Battle cry 3 | 9 | Attack grunt 1 | 16 | Critical hit | 23 | Unlock failed |
| 3 | Battle cry 4 | 10 | Attack grunt 2 | 17 | Target immune | 24 | Unlock success |
| 4 | Battle cry 5 | 11 | Attack grunt 3 | 18 | Lay mine | 25 | Separated from party |
| 5 | Battle cry 6 | 12 | Pain grunt 1 | 19 | Disarm mine | 26 | Rejoined party |
| 6 | Select 1 | 13 | Pain grunt 2 | 20 | Begin stealth | 27 | Poisoned |
Warning
The trailing count is not uniform, and Rakata currently normalises it Real files carry extra
-1values 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 atindex != 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 readingsound_table_offsetlive out of the loaded buffer rather than assuming a fixed position. Its only caller,GetSoundSetStrres(0x0060b8a0), andPlaySoundSetSound(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 Event | Ghidra Provenance & Engine Behavior |
|---|---|
| Finding the Table | The 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 Slots | Starting 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 Blanks | Obviously, 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
1behind the scenes to correctly navigate the literal0-indexedarray in memory.