MacProject Pro 1.5 file format

This page is docs/file-format.md from the repository, published as written.

How MacProject Pro 1.5 stores a project (file type MPRX, creator MPRP) and a Perspective (MPRE), as worked out for Glass Project. MPXKit reads and writes it (Container, Decoders, Encoders, WriterSync).

Everything here came from reverse engineering, for interoperability: reading the application's code and resources, decoding the 31 project files that ship with it, and checking results in MacProject Pro 1.5 itself, running under emulation. Names like CReadWrite_1844 are labels for functions in the original's code, given so a finding can be traced to where it was read. Marked guess where a meaning is inferred rather than confirmed.

See also scheduling.md (how dates are calculated), leveling.md (the resource leveler) and behavior.md (menus, shortcuts and gestures).

Field type codes (CFileAccess_2a40)

Most codes read or write a fixed byte count through the stream's vtable method at +0x18. Stream object: +0 creator, +4 file type, +8 file version (short), +0xa format class, +0xb mode (1 when reading), +0xc vtable. All confirmed from the code and against the samples unless marked.

Code On disk
0 Handle: length (u32 from version 0x13b, else u16), then
1 10: SANE 80-bit extended (guess from size)
2, 3, 8, 0x14, 0x19, 0x1b, 0x20 4. 2 is IEEE float32 in every sample field (costs, rates, counts, work: 87.5, 1/48, 140000) except shap +0x94 (bit patterns) and the cbkt amounts (below). 8 is an object reference {u16 kind, u16 index}; index 0xFFFF = none, 0xFFFFFFFF = null (all 24,314 sample values fit: 340 null, the rest known kinds). 0x1b holds durations in seconds (0x31380 = 56 h)
4, 9, 0x17, 0x1d 2 (short)
5, 0x12 1 (byte / Boolean)
6 String kept in the data manager: s16 length, then bytes when length > 0
7, 0x15, 0x1e 8. 0x15 holds two longs in tasks (chart position, guess)
0x0a–0x11 one Boolean byte per flag bit 0x80…0x01, packed into one byte in memory
0x13 Pascal string: s16 length, then (length & 0xff) bytes
0x16 16 (four longs in tasks: a rect, guess)
0x18 Field id (short). In Perspective files (version > 0x141) an id > 0xfff (user field) is followed by its name (code 6) and data type (short)
0x1a Date: u32 seconds since 1904-01-01, truncated to whole minutes. 0 = none, 0xFFFFFFFF special, nonzero values below 0x80000000 (before 1972) clamped to 0x80000000
0x1c 6: RGBColor (u16 red, green, blue)
0x1f Object kind as its 4-char tag (FUN_001433a8)
0x21 256: Str255
0x22 64: Str63

Composite encodings used by the serializers:

Project file format (type MPRX, data fork; MPRE = Perspective)

Creator MPRP. FUN_00142df0 reads the u16 version at offset 0; a version from 301 to 329 (0x12d–0x149) is the current format (class 4). New streams start at version 0x149 (FUN_00142cfc; guess: the version 1.5v1 writes); the samples are 0x148, two are 0x149. Other types and versions (MPRD, MPRC, MPRH, versions 0xd2–0xd6, 0x61, or a 0x1e61 marker at 0xa2) select older format classes 1–3, presumably MacProject II files (guess); those paths are not decoded.

Perspective files (MPRE) use the same header but only list 15: one prsp record at 0x110 (no gap), and the header count is not the record count (8 in every sample). CFileAccess_3258 reads only slot 15 for MPRE.

Record bodies

MPXKit's Decoders implement every body below field by field, with labels that are the field's offset in the in-memory object, so each one maps back to the original's serializer. Field meanings are mostly not decoded yet. Kind → class → serializer, from CConstruct_0540 and the vtables:

Tag Constructor Vtable Serializer (vptr+0x2c) Notes
outl CConstruct_18d0 ffffdf18 CReadWrite_59e4 pre-read CReadWrite_6068 (vptr+0x84); base CReadWrite_0bd8
smry CConstruct_1b30 ffffdfa4 CReadWrite_60bc = 59e4 + more
task CConstruct_1bc8 ffffe034 CReadWrite_6348 = 60bc + more; pre-read CReadWrite_6780. A class short (vptr+0x78) is stored and the object is re-classed from it on read (CTask_0824); Supertasks (Type 6) add a trailing string
extk CConstruct_0d1c ffffc808 CReadWrite_2654 = task + document info; Type 7 (External)
olvl CConstruct_1d4c ffffe0c8 CReadWrite_6954
note CConstruct_0fd8 ffffcbe4 CReadWrite_3ef0 base CReadWrite_3da8: subtype short picks the value's type
uatt CConstruct_109c ffffcc58 CReadWrite_3ff0 nested only
idep CConstruct_09a8 ffffc370 CReadWrite_1120 nested only
iall CConstruct_0120 ffffaecc CReadWrite_008c nested only; leads with its own total length (version > 0x146)
cal CConstruct_03d4 ffffb95c CReadWrite_0630 7 weekday records, holidays; CReadWrite_0584 variant when the data manager says so
rsrc CConstruct_13c0 ffffd69c CReadWrite_4b08 CReadWrite_4a84 variant likewise
subp / supr / scop / edoc CConstruct_0aa0 / 0bb8 / 0c68 / 0db8 ffffc760 / c798 / c7d0 / c914 CReadWrite_2460 / 254c / 25a4 / 2778 all start with document info (FUN_000bc230: two Str63 names, …)
attr CConstruct_0218 ffffb470 CReadWrite_046c user field: id, data type, …
anam CConstruct_0298 ffffb4a8 CReadWrite_04cc field id, name, short
prsp CConstruct_1168 ffffd038 CReadWrite_4528
shap CConstruct_1588 ffffd910 CReadWrite_53c0 inline mark (FUN_000beb70) and bar (FUN_000becc8) items
srch CArrayConstructor_0884 ffffcfb4 CReadWrite_436c
row CArrayConstructor_0974 ffffd6d4 CReadWrite_4e28
txst CRes_3750 ffffdbcc CReadWrite_55b0 font ref, face, size, RGBColor
font CConstruct_1804 ffffdc04 CReadWrite_5608 font name (code 0x13)
cbkt CCost_0c98 ffffc068 CReadWrite_1058
dcst (document data manager) ffffc438 CReadWrite_1844 (DM vptr+0x40) pre-read CReadWrite_15b8 (DM vptr+0x4c), then CReadWrite_1254
cary (document data manager) ffffc438 CReadWrite_1f2c (DM vptr+0x28) counted 4-byte refs

hday, srun, atst, bar , mark have no serializer of their own (the base CReadWrite_1118 is empty); they are written inline by their owners. pbsb, txt and proj have no case in CConstruct_0540; not seen in the samples.

Validation (SampleTests): all 31 files (23 projects, 8 Perspectives), 5,016 records, every body decoded to its exact length; all 902 nested iall length fields agree. edoc, extk, row and scop never occur in the samples, so their layouts come from the code only.

Two samples carry damaged tags inside a task's object list: Conference Planning ('ia&&' in the task at 0x33c7) and Fund-raising Campaign ('Iall' in the task at 0x3d7d). The bodies are iall ones (their length fields agree), and MPXKit reads them as such and reports it. The app would map these tags to whatever is left in D7 (low byte 1 in CFileAccess_3044), so it may fail to open these two files; not yet tried in MacProject Pro itself.

Field encodings seen so far:

Field names

Sources, all confirmed against each other:

Field names: the file's anam names win, and the application's built-in names (its ATNM resources, MPXKit's defaultFieldNames) are the fallback. References are written tag#index.

Structure found from the getters and checked in the samples:

Settings, from the dialogs. Each dialog is a MacApp view resource (view id, class, title); the dialog code stores each control's value, by view id, into a document or Perspective field or flag bit:

Checked in MacProject Pro itself (2026-10-17, a copy of Business Start-up):

Not named yet: calendar internals (weekday table, flags), note flags other than +0x34/+0x36, shape and mark details, some dcst/prsp fields (the date-display block at +0x94/+0x158 is a guess from CReadWrite_50cc), and in tasks +0x18, +0x9a–+0x9e, +0xa3/+0xa6/+0xa7, +0xa8 flags, +0xc8/+0xca, most +0x106/+0x107 flags, and the dates +0xe4…+0xf8. Those dates look like the scheduler's working values: across all 619 sample tasks +0xf4 equals Earliest Start and +0xf0 Earliest Finish, +0xf8 equals Actual Finish, +0xec equals Earliest Start except on start milestones with a start constraint (where it keeps an older date), and +0xe4/+0xe8 hold placeholders (1987– 1991-01-01) or older dates. Searching code by offset doesn't settle them: CAnalysis uses the same offsets on its own node structure (task pointer at node +0xde), and so does the MPX importer. The aete doesn't help: it exposes tables whose columns are these same attributes, by anam name.

SampleTests decodes every sample and checks that each body is read to its last byte; WriterTests writes each one back byte for byte.