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

PT Format (Party Table)

The partytable (PARTYTABLE.res) file is a live snapshot of the adventuring group plus a grab-bag of session state (type PT , two trailing spaces). It sits loose in the save folder, written at save time.

At a Glance

PropertyValue
FilenamePARTYTABLE.res
Magic SignaturePT / V3.2 (two trailing spaces in the tag)
TypeParty Table
Rust ReferenceHandled by rakata-save (mid-refactor).

File Layout

The top-level struct groups party state into scalar flags plus several lists: the roster, available companions, the pazaak decks, the feedback and dialog logs, and the journal. Each group is a field table below.

Engine Audits & Decompilation

(Documented from Ghidra decompilation of swkotor.exe (K1 GOG build). Writer: CSWPartyTable::SaveTableInfo at 0x005648c0. The journal is written by CSWPartyTable::SaveJournal at 0x00563d90, which SaveTableInfo invokes. Provenance: derived, not attested, so these rows sit on the reverse-engineering queue.)

Resources and flags

FieldTypeMeaning
PT_GOLDDWORDParty gold. The authoritative value on load: party members’ own Gold fields are deliberately skipped in favour of this one (see Gold and the party pool).
PT_XP_POOLINTShared experience pool; benched companions are topped up toward their npc.2da PercentXP share of it when they rejoin.
PT_PLAYEDSECONDSDWORDRunning play time, in seconds.
PT_CHEAT_USEDBYTECheat flag; savenfo’s CHEATUSED carries the same value.
PT_SOLOMODEBYTESolo-mode flag.
PT_CONTROLLED_NPINTCurrently controlled party member.

Roster

PT_NUM_MEMBERS (BYTE) plus PT_MEMBERS, a list with one struct per active member:

FieldTypeMeaning
PT_MEMBER_IDINTCompanion id (npc.2da row) of the member.
PT_IS_LEADERBYTEWhether this member is the party leader.

Available companions

PT_AVAIL_NPCS, a list with one struct per recruitable companion, indexed by npc.2da row (nine slots in K1):

FieldTypeMeaning
PT_NPC_AVAILBYTEWhether the companion has been unlocked.
PT_NPC_SELECTBYTEWhether the companion is selectable for the active party.

These flags are only the index. The actual creature snapshot for each recruited companion is a standalone AVAILNPCn UTC bundled inside SAVEGAME.sav, written at recruit time and refreshed at save time; PT_NPC_AVAIL gates whether the engine will instantiate it at all. See the companion pool in the deep dive.

Party AI

FieldTypeMeaning
PT_AISTATEINTParty combat-AI state.
PT_FOLLOWSTATEINTParty follow/formation state.

Galaxy map

FieldTypeMeaning
GlxyMapNumPntsDWORDNumber of known map points.
GlxyMapPlntMskDWORDPlanet unlock bitmask.
GlxyMapSelPntINTCurrently selected map point.

Pazaak

FieldTypeMeaning
PT_PAZAAKCARDSlistOwned-card counts: a fixed 18 elements, each { PT_PAZAAKCOUNT: INT } (one per card).
PT_PAZSIDELISTlistChosen side deck: a fixed 10 elements, each { PT_PAZSIDECARD: INT }.

Feedback and dialog logs

FieldTypeMeaning
PT_FB_MSG_LISTlistOn-screen feedback messages, each { PT_FB_MSG_MSG: CExoString, PT_FB_MSG_TYPE: DWORD, PT_FB_MSG_COLOR: BYTE }.
PT_DLG_MSG_LISTlistDialog message log, each { PT_DLG_MSG_SPKR: CExoString, PT_DLG_MSG_MSG: CExoString }.
PT_COST_MULT_LISlistStore cost multipliers, each { PT_COST_MULT_VAL: FLOAT }.

UI state

FieldTypeMeaning
PT_TUT_WND_SHOWNVOIDTutorial-window-shown flags (opaque byte blob).
PT_LAST_GUI_PNLINTLast GUI panel the player had open.

Journal

The journal is folded into the same file: JNL_SortOrder (INT) plus JNL_Entries, a list with one struct per active quest:

FieldTypeMeaning
JNL_PlotIDCExoStringQuest/plot identifier.
JNL_StateINTCurrent quest state.
JNL_DateDWORDIn-game date stamp.
JNL_TimeDWORDIn-game time stamp.

Note

partytable quirks.

  • The journal block is omitted entirely when the party journal is empty. A reader must treat a missing JNL_Entries as “no active quests”, not as malformed data.
  • PT_TUT_WND_SHOWN is a GFF VOID field (an opaque byte blob), not an integer.
  • PT_PAZAAKCARDS has a stowaway element: after the 18 INT card-count entries, the writer appends a 19th entry whose PT_PAZAAKCOUNT is a BYTE carrying the cheat-used flag, not a card count. The loader reads back only the first 18 (as INT) and ignores the rest, so that trailing byte never round-trips. Read 18 elements; ignore any trailing one. This is not a hidden store: the cheat flag’s real home is the top-level PT_CHEAT_USED field, which does round-trip. The trailing byte is a dead write (a BYTE among 18 INTs, sourced from the cheat-flag field), most likely leftover code.

Note

Three of the labels above are shorter than they read. A GFF label is a fixed sixteen bytes, so PT_CONTROLLED_NP, PT_COST_MULT_LIS and PT_COST_MULT_VAL are not typos or abbreviations of longer names. The longer spellings cannot exist on disk, and every save carries the sixteen-byte forms. A reader that asks for the fuller name finds nothing and takes the absent default, which for the first two means a sentinel and an empty list off a file that holds both.

Absent-value behaviour

What the engine holds when a label is missing. Traced from CSWPartyTable::LoadTableInfo (0x00565d20), LoadJournal (0x00563430), and ClearTable (0x00563200), which runs unconditionally before any field is read and so establishes the baseline every “whole list or struct absent” case below falls back to. Provenance: derived, not attested.

Read with a plain literal default. PT_GOLD, PT_XP_POOL, PT_CHEAT_USED, PT_AISTATE, PT_FOLLOWSTATE, PT_NPC_AVAIL, PT_NPC_SELECT, GlxyMapNumPnts, GlxyMapPlntMsk, JNL_State, JNL_Date, JNL_Time, all default 0. PT_CONTROLLED_NP, PT_MEMBER_ID, GlxyMapSelPnt default -1, a real sentinel in each case rather than an arbitrary choice, matching pt_leader_id/galaxy_map_selected_planet’s own ClearTable baseline. PT_IS_LEADER defaults 0, so an absent field never promotes a member to leader. PT_TUT_WND_SHOWN (the VOID blob) defaults to six zero bytes copied in. JNL_PlotID defaults "", same as PT_FB_MSG_MSG, PT_DLG_MSG_SPKR, and PT_DLG_MSG_MSG.

Two fields are pure skips, chained off something other than this. PT_SOLOMODE defaults to its own current value (this->pt_solomode), an unchanged read, though a result of 0 (read or defaulted) always triggers UnstealthParty regardless of which one produced it. PT_LAST_GUI_PNL defaults to the live in-game GUI’s own current panel (CClientExoApp::GetInGameGui()->last_gui_panel), not a party-table field at all. JNL_SortOrder defaults to a process-global (journalSortOrder), also not a party-table field.

PT_PLAYEDSECONDS has an undocumented sibling fallback. When PT_PLAYEDSECONDS is absent, the loader doesn’t stop at a literal default. It reads a second field, PT_PLAYEDMINUTES, and multiplies it by 60. PT_PLAYEDMINUTES is not in the field table above, because it exists only as this fallback’s own source. It defaults to 0 if it is absent too, so the floor is the same 0 every other DWORD here lands on, just reached by a different route.

PT_NUM_MEMBERS is clamped twice, not just capped at the u8 boundary. It is a BYTE, so it cannot exceed 255 on disk whatever a writer intends. The loader then clamps it a second, independent way after the read: it’s brought down to PT_MEMBERS’s own list length if the list has fewer elements than the declared count claims, and never brought up if the list has more. An absent PT_MEMBERS list clamps the count to 0 even if PT_NUM_MEMBERS itself reads back nonzero, and the member-reading loop simply never runs, so pt_member_ids/pt_leader_id keep ClearTable’s baseline (0x7f000000 sentinel ids, leader -1).

PT_AVAIL_NPCS’s nine-slot cap is confirmed the same way, from the other direction. The loader takes the smaller of the list’s actual count and 9, so a list claiming more than nine elements is truncated to nine rather than rejected. An absent list clamps to 0 and the per-slot loop never runs at all, so pt_avail_npcs[] (companion unlocked) keeps ClearTable’s baseline of all-0, but pt_selected_npcs[] (companion selectable) keeps a baseline of all-1. Those two arrays default to opposite states: unlocked defaults closed, selectable defaults open.

GlxyMap absent skips the whole block, and the derived planet array has its own gate on top. If the GlxyMap struct itself is missing, none of its three fields are read and this->selectable_planets[] keeps ClearTable’s all-0 baseline (no planets marked selectable). Even when the struct is present, the derived per-planet unlock array is only populated if GlxyMapNumPnts == 16 exactly. Any other value, including its own absent-default of 0, skips the bitmask-unpacking loop entirely and leaves selectable_planets[] untouched. This is a hard equality gate, not a presence check.

PT_PAZAAKCARDS and PT_PAZSIDELIST are read unconditionally, with no “list found” guard, and that erases a non-zero starting baseline. Both loops run their fixed length (18 and 10 respectively) regardless of whether GetList found anything, reading each element independently with ReadFieldINT’s own default of 0. That matters because ClearTable’s baseline for these two is not zero: it seeds the first five Pazaak card-count slots to 2 each (a starter deck) and every side-deck slot to -1 (empty). A writer that omits either list entirely produces a table read back as zero starter cards and ten side-deck slots holding card index 0, not the starter-deck baseline. That is a real behavioural difference from “the field was never touched”, and invisible until a save is reloaded.

PT_FB_MSG_LIST and PT_DLG_MSG_LIST are the simple case. Both are gated on the list’s own element count; an absent list reads back as zero elements and neither message buffer is touched.

PT_COST_MULT_LIS loops by a count that has nothing to do with the list itself. The loop bound is the live base-item table’s count (baseitems.2da’s row count at runtime), not GetListCount() on this field, and there’s no “list found” guard around it either. An absent list means every base-item type’s cost multiplier reads back at the field’s own default, 1.0, a deliberate “no override” baseline rather than a coincidental zero.

JNL_Entries absent is a true no-op, confirming the existing note above with the mechanism. LoadJournal gates its entire body on GetList succeeding; when it doesn’t, the function returns having touched nothing, not even to clear a pre-existing journal. The existing “no active quests” phrasing describes the outcome for a fresh load correctly, but the mechanism is closer to “the loader does nothing at all” than “explicitly sets empty.”

Implemented Linter Rules (Rakata-Lint)

None yet. Documented here ahead of any dedicated rakata-lint rules.