ChangeEdit is a single-process, single-window Delphi 7 VCL desktop application.
It is not MDI — the main window uses a TTreeView on the left and a set of stacked TPanels on the right that are shown/hidden based on the selected tree node.
ChangEd.dpr creates all 13 forms at startup via Application.CreateForm and then calls Application.Run. Every form is a global singleton; none are created on demand.
Application
└── TMainForm (MainForm) ← only form that is ever "visible" first
└── TEditTokenForm
└── TMod2DARowForm
└── TNewLabelForm
└── TChange2daForm
└── TAdd2daColForm
└── TInfoForm
└── TInfoCopyForm
└── TMassAddForm
└── TFormNewGFF
└── TFormFilename
└── TNamespaceForm
└── TCopy2daForm
┌──────────────────────────────────────────────────────────────────┐
│ Presentation layer (VCL forms) │
│ UMainForm + 13 modal dialog forms (U*Form.pas) │
├──────────────────────────────────────────────────────────────────┤
│ Domain / file-format layer │
│ U2DAEdit UGFFFile UTLKFile UST_IniFile │
├──────────────────────────────────────────────────────────────────┤
│ Utilities │
│ UST_Common UStrTok │
├──────────────────────────────────────────────────────────────────┤
│ Delphi 7 RTL + VCL (Windows, SysUtils, Classes, Grids, …) │
└──────────────────────────────────────────────────────────────────┘
There is no MVC separation. UMainForm directly reads from and writes to the loaded TST_IniFile instance (ini : TST_IniFile) and calls the file-format units as needed.
| Unit | Form class | Purpose |
|---|---|---|
UMainForm |
TMainForm |
Main window; tree navigation + 8 stacked section panels; holds the live TST_IniFile and orchestrates all load/save/edit operations |
UEditTokenForm |
TEditTokenForm |
Modal — enter a TLK token: StrRef + token value |
UMod2DARowForm |
TMod2DARowForm |
Modal — modify a 2DA row: column selector + value grid |
UChange2daRowForm |
TChange2daForm |
Modal — change a specific 2DA cell by row/column label |
UCopy2daRowForm |
TCopy2daForm |
Modal — copy a 2DA row |
UAddColForm |
TAdd2daColForm |
Modal — add a new 2DA column with a label and default value |
UMassAddForm |
TMassAddForm |
Modal — bulk-import files from a folder |
UFormNewGFF |
TFormNewGFF |
Modal — add or edit a GFF field (type-specific controls, localised string support) |
UFormFilename |
TFormFilename |
Modal — generic filename input with file browser |
UModLabelForm |
TNewLabelForm |
Modal — enter a section/modifier label string |
UNamespaceForm |
TNamespaceForm |
Modal — manage namespace → INI/RTF file path mappings |
UInfoForm |
TInfoForm |
Non-modal display — multi-tab help text for each section |
UInfoCopyForm |
TInfoCopyForm |
Modal — read-only TMemo for copy-able text output |
| Unit | Key class(es) | Purpose |
|---|---|---|
U2DAEdit |
T2DAHandler |
Reads and writes binary 2DA v2.b files. Internal storage: parallel array of string for column labels, row labels, and a 2-D entries array. Exposes array-property accessors (clabels[c], rlabels[r], entry[r,c]). Custom exception: EDead. |
UGFFFile |
TGFFFile, TGFFStruct, TGFFList, TGFFField + 16 typed subclasses |
Full GFF v3.2 parser/emitter. 18 field types (BYTE through POSITION). Nested struct/list tree. Custom exception: EGFFError. Known gap: no write support for DWORD64 or VOID fields. |
UTLKFile |
TTLKFileHandler, TTLKString |
Reads and writes the dialog .tlk binary format. Each entry holds flags, sound resref, string offset, and the string itself. Custom exception: EHell. |
UST_IniFile |
TST_IniFile |
Extends the standard Delphi TIniFile with custom escape sequences (\n → line-feed, \r → carriage-return) needed for multi-line INI values used by TSLPatcher. |
| Unit | Key class | Purpose |
|---|---|---|
UST_Common |
— | Free-standing helper functions: ShowAlertBox, ShowConfirmBox, common string/file utilities, Windows system folder access |
UStrTok |
TStringTokenizer |
Simple delimiter-based string tokenizer; used by UGFFFile and UFormNewGFF for path splitting |
The left-side TTreeView (named tree) contains 8 top-level nodes. Clicking a node calls treeChange(), which hides all panels and shows exactly one:
| Tree node | Visible panel | Grid(s) used |
|---|---|---|
| Changes | (sub-nodes only) | — |
| Settings | paneSettings |
none (plain edit controls) |
| TLK | paneTLK |
gridTlk, gridTokens |
| 2DA | pane2da |
grid2daMod |
| GFF | paneGFF |
gridGffMod |
| Install | paneInstall |
gridInstall |
| Script | paneScript |
gridScript |
| SSF | paneSSF |
gridSSF |
The live state of the application is a single TST_IniFile instance (ini) that mirrors the on-disk changes.ini. All editing operations read from and write to this object; the physical file is only touched on explicit Save.
TST_IniFile (ini)
├── [Settings] key/value pairs (Caption, Confirm, Mode, Backups, …)
├── [TLK] StrRef=Token entries + StrRefList
├── [filename.2da] per-file subsections
│ ├── AddRow_N new row entries
│ ├── ModRow_N cell modifications
│ ├── CopyRow_N row clones
│ └── AddCol_N column additions
├── [filename.gff] per-file subsections
│ └── FieldPath=TypedValue entries
├── [SSF] soundset field assignments
├── [Script_N] .nss → .ncs compilation instructions
└── [InstallList] file-copy directives
ChangEd.dpr
│
├─► UMainForm
│ ├─► U2DAEdit
│ ├─► UGFFFile
│ │ └─► UStrTok
│ │ └─► UST_Common
│ ├─► UTLKFile
│ ├─► UST_IniFile
│ │ └─► UST_Common
│ └─► UST_Common
│
├─► UMod2DARowForm ──► U2DAEdit, UST_IniFile
├─► UChange2daRowForm ► U2DAEdit, UST_IniFile
├─► UCopy2daRowForm ─► U2DAEdit, UST_IniFile
├─► UAddColForm ─► U2DAEdit
├─► UFormNewGFF ─► UST_IniFile, UST_Common, UStrTok
├─► UNamespaceForm ─► UST_IniFile
└─► UST_IniFile (direct)
"2DA V2.b" LF 8-byte header + newline
<TAB-separated column names> NUL column label block
<32-bit DWORD> row count
<TAB-separated row names> row label block (no NUL)
<16-bit WORD grid [rows × cols]> offset table into data section
<2 padding bytes>
<NUL-terminated strings> data section (cell values)
Empty cells are stored as a single NUL byte. The **** sentinel in TSLPatcher means "do not modify this cell" and is preserved as a literal string.
Standard BioWare GFF: header → struct array → field array → label array → field data → field indices → list indices. TGFFFile.LoadFile reads all sections into a tree of TGFFStruct/TGFFList/TGFFField objects. SaveFile regenerates all sections from the object tree.
The 18 supported field types map to Delphi classes:
0 BYTE → TGFF_SByte
1 CHAR → TGFF_SChar
2 WORD → TGFF_SWord
3 SHORT → TGFF_SShort
4 DWORD → TGFF_SDWORD
5 INT → TGFF_SInt
6 DWORD64 → TGFF_CDWORD64 (read-only — no write support in D7)
7 INT64 → TGFF_CInt64
8 FLOAT → TGFF_SFloat
9 DOUBLE → TGFF_CDouble
10 CExoString → TGFF_CExoString
11 ResRef → TGFF_CResRef
12 CExoLocString→ TGFF_CExoLocString (with TGFF_CSubString per language)
13 VOID → TGFF_CVoid (read-only — no binary write support)
14 Struct → TGFFStruct
15 List → TGFFList
16 Orientation → TGFF_COrientation
17 Position → TGFF_CPosition
The UMainForm comment acknowledges: "textbook example of an unplanned, unstructured hack."
Specific issues to be aware of when editing:
- All 13 forms are created at startup — avoid adding heavyweight initialisation to
FormCreatehandlers. UMainFormmixes file I/O, validation, and UI update in the same event handlers.- Dialogs share state via
publicproperties and field-level variables (box_2daname,box_ini). l_prefix variables inTMainFormare conceptually private but may be read across units.
| Member | Kind | Description |
|---|---|---|
Load2daFile(sFilename) |
procedure | Open and parse a binary 2DA v2.b file into internal arrays |
Save2daFile(sFilename) |
procedure | Write the current state to disk as a binary 2DA v2.b file |
AddLine() |
function → integer | Append a new blank row; return its 0-based index |
AddColumn() |
function → integer | Append a new blank column; return its 0-based index |
CloneLine(iIndex, sNewLabel) |
function → integer | Duplicate row iIndex with an optional new label; return the new row index |
GetColByLabel(sLabel) |
function → integer | Find a column index by its label string; −1 if not found |
GetRowByLabel(sLabel) |
function → integer | Find a row index by its label string; −1 if not found |
rowcount |
property → integer | Number of data rows |
colcount |
property → integer | Number of columns |
clabels[c] |
property string (r/w) | Column label at index c |
rlabels[r] |
property string (r/w) | Row label at index r |
entry[r, c] |
property string (r/w) | Cell value at row r, column c |
Exception on any parse error: EDead.
| Member | Kind | Description |
|---|---|---|
LoadFile(sFilename) |
procedure | Read a binary GFF v3.2 file into an object tree |
SaveFile(sFilename) |
procedure | Write the current object tree back to disk (optional filename; defaults to original) |
NewFile(sType, sFilename) |
procedure | Create an empty GFF file with the given 4-char type tag |
GetFieldByLabel(sFieldPath) |
function → TGFFField | Return the field at a dot-separated path (e.g. "Struct.FieldName") |
GetFirstRootField() |
function → TGFFField | Iterator: return the first field of the root struct |
GetNextRootField() |
function → TGFFField | Iterator: return the next field of the root struct |
AddField(oField, sPath) |
procedure | Insert a field at the given path |
DeleteField(sFieldPath) |
procedure | Remove a field by path |
ChangeFieldValue(sPath, sValue) |
function → boolean | Update a field's value by path; returns false if path not found |
Exceptions: EGFFError. See FORMATS.md for field type constants and write limitations.
| Member | Kind | Description |
|---|---|---|
Create() |
constructor | Empty handler |
Create(sFilename) |
constructor | Open and parse a TLK file immediately |
LoadTlkFile(sFilename) |
procedure | Parse a binary TLK v3.0 file |
SaveTlkFile(sFilename) |
procedure | Write current entries to disk (strips read-only first) |
NewTlkFile() |
procedure | Reset to an empty TLK |
AddEntry(oEntry) |
procedure | Append a TTLKString entry |
ReplaceEntry(oEntry) |
procedure | Replace an existing entry by StrRef |
Reset() |
procedure | Clear all loaded data |
strings |
property → TStringDataList | Doubly-linked list of all TTLKString entries |
count |
property → DWORD | Number of string entries |
fileid |
property → T4Char | 4-char file type tag ("TLK ") |
version |
property → T4Char | 4-char version tag ("V3.0") |
fileexists |
property → boolean | True if a file has been loaded |
language |
property DWORD (r/w) | Language ID of the file |
TTLKString properties: strflags, strsound, sndvolume, sndpitch, stroffset, strsize, sndlength, strtext, strref, iscustom.
TStringDataList iteration: call first() then next() in a loop; check eol() to stop. Use Insert(oEntry) and Delete(bFreeObject) to modify.
Exception on parse error: EHell.
Extends Delphi's TIniFile. All inherited methods work normally; the three overrides add TSLPatcher escape-sequence handling.
| Member | Kind | Description |
|---|---|---|
ReadString(Section, Ident, Default) |
function → string | Read a value; converts <#LF#> → Chr(10), <#CR#> → Chr(13) |
WriteString(Section, Ident, Value) |
procedure | Write a value; converts Chr(10) → <#LF#>, Chr(13) → <#CR#> |
ReadSectionValues(Section, Strings) |
procedure | Read all key=value pairs in a section; applies same escape conversion to every value |
All other TIniFile methods (ReadInteger, WriteInteger, ReadBool, SectionExists, ReadSection, etc.) are inherited unchanged.
| Member | Kind | Description |
|---|---|---|
Create(sString, sToken) |
constructor | Parse sString using sToken as the single-char delimiter; store tokens internally |
first() |
function → string | Reset iterator; return the first token |
next() |
function → string | Advance iterator; return the next token, or '' at end |
strings[i] |
property string (default) | Indexed token access |
count |
property → integer | Number of tokens |
current |
property → integer | Current iterator position |
delimiter |
property → Char | The delimiter character |
text |
property → string | The original full string |
Notes: strings shorter than 3 chars are stored as-is without splitting. Empty tokens (consecutive delimiters) are filtered out.
Version 2.0 (last changed 2006-05-17).
Message dialogs
| Function | Return | Description |
|---|---|---|
ShowAlertBox(sMessage) |
word | Warning dialog (MB_OK). Use instead of ShowMessage. |
ShowInfoBox(sMessage) |
word | Information dialog (MB_OK) |
ShowConfirmBox(sMessage) |
word | Confirmation dialog (MB_YESNO); returns mrYes or mrNo |
Number validation
| Function | Return | Description |
|---|---|---|
GetIsNumber(sStr) |
boolean | True if sStr is a valid unsigned integer |
GetIsNumberSigned(sStr) |
boolean | True if sStr is a valid signed integer |
GetIsFloat(sStr) |
boolean | True if sStr is a valid decimal number (accepts , or .) |
Safe type conversion
| Function | Return | Description |
|---|---|---|
SafeStrToDouble(sStr) |
Double | Locale-safe — normalises ,/. to the system decimal separator before conversion |
SafeStrToFloat(sStr) |
Single | Same as above, returns Single |
SafeStrToInt(S) |
Integer | Handles the DWORD maximum 4294967295 by converting via hex (Delphi 7 limitation workaround) |
File operations
| Function | Return | Description |
|---|---|---|
MakeFileWritable(sFilename) |
— | Remove the read-only attribute from a file |
GetFileIsWriteProtected(sFilename) |
boolean | True if the file has the read-only attribute |
BackupFile(sFilename, sNewfile) |
— | Copy sFilename to sNewfile (despite the name, this is a copy, not a backup-rotate) |
OpenFolderDialog(sTitle, iFlags) |
string | SHBrowseForFolder-based folder picker; returns selected path or '' if cancelled |
System paths
| Function | Return |
|---|---|
GetWindowsDir() |
Windows directory path |
GetTempDir() |
Temp directory path |
GetSystemDir() |
System32 directory path |
Folder operations
| Function | Return | Description |
|---|---|---|
DeleteFolder(sFolder, bRecursive) |
boolean | Delete a folder, optionally recursively |
GetFilesInFolder(sFolder, bNameOnly) |
TStringList | List filenames (or full paths) in a folder; caller must free the list |
String utilities
| Function | Return | Description |
|---|---|---|
ReplaceInString(sSource, sFind, sReplace) |
string | Brute-force substring replacement; recursive implementation |
StringToResRef(sText) |
string | Truncate to 16 chars, lowercase, strip non-alphanumeric/underscore |
Shell / system
| Function | Return | Description |
|---|---|---|
RunShellGetOutput(sExe, sParams, sWork) |
string | Run a console executable with Win32 pipes; capture stdout; wait for completion. Read buffer: 2400 bytes |
GetFileTypeSmallIcon(sFilename) |
TIcon | Shell small icon for a file's extension |
GetFileTypeLargeIcon(sFilename) |
TIcon | Shell large icon for a file's extension |
GetFileSizeString(iBytes) |
string | Human-readable size: "N bytes" or "N kB" |
GetRegistryString(sKeyName, sValue) |
string | Read a string value from HKEY_LOCAL_MACHINE |