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:
- Object list (
CFileAccess_3044): repeat {u8 end flag; if 0: kind tag, u16 index, that kind's body}. Tasks nestidep,iall,uattthis way (notetoo perFUN_00143acc, but no sample does); Perspective files nestsrch. - Counted array (
CFileAccess_2d34,CFileAccess_2e50,CArray_3690): s16 count, then that many items. - Optional item (
CFileAccess_2c78): presence byte, then the item. - Attribute set (
FUN_000bf118): kind tag (atst) + counted {field id, object ref [, text style when the ref is to ansrun]}. - Text style (
FUN_000bf214): kind tag, then a counted run list forsrunor an inlinetxstbody. - Value whose type depends on a field id (
srch,row): data type from the built-in field table (byte 1 of each 4-byte entry at 0x8d004, ids 0–0x73) or, for user fields (id > 0xfff), from theattrrecord; data type → stream code viaCFileAccess_0e30.
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.
0x000u16 version0x00227 list slots × {u16 count, u32 head, u32 tail} (26 slots before version 0x139). Slot → kind (FUN_00141a04): 0font, 1txst, 2attr, 3anam, 4srch, 5dcst, 6cal, 7scop, 8rsrc, 9shap, 10outl, 11task, 12note, 13subp, 14supr, 15prsp, 16extk, 17edoc, 18cbkt, 19 kind 0x80 (no tag), 20note, 21pbsb, 22smry, 23olvl, 24row, 25cary, 26task(a second order over the same task records). The app reads slots 0–25 and followsnextlinks until 0; it never reads the counts.0x110u16 (zero) in projects; optional unlinkedMPBnblocks (tag, u16, u16 index, u16 flags, u32 length, payload) follow in files with subprojects (10 blocks, in Product Development Plan and Prototype Subproject). The read path for them is not found yet.- then records, back to back to EOF. Header (
FUN_00142a90): u32 next, u32 prev; lists 11 and 16 add a second u32 pair (for tasks, the list 26 links); then the kind tag and a u16 index. The body follows. - Records are byte-packed (odd offsets are normal).
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:
- Strings: code 6 (s16 length + MacRoman bytes) for task, resource and field names; 0x13 for font names; Str63 inside document info.
- Dates: code 0x1a.
0xA3A58180(1991-01-01 00:00) appears as a base/placeholder date. - Durations: code 0x1b, seconds of work time (0x7080 = 8 h day, 0x23280 = 40 h week).
- Quantities: IEEE float32 in code 2 fields (
3F800000= 1.0). - Cost buckets (
cbkt) keep 80-bit SANE extended amounts in memory (CDataObject_0b64copies 10 bytes), butCReadWrite_1058writes them with code 2, so the file holds only the first 4 bytes: sign/exponent and 16 mantissa bits including the explicit integer bit (40 10 88 b8= 140,000). Bucket totals match the tasks' costs (Magazine Publishing: 131,848 vs 131,850 planned cost, 140,000 vs 140,000 income). txstfont field 0x8001 (guess: high bit = index into thefontrecords).
Field names
Sources, all confirmed against each other:
- The built-in field table, 4 bytes per field id 0–0x73 at 0x8d004 (in CParser): flags, data type, u16 offset in the object. Accessors next to it: 0x8d1d4 offset, 0x8d1e6 data type, 0x8d1f8 flags & 0x30, 0x8d20e flags & 0x40, 0x8d224 flags & 0x0f. Offset 0 means a computed field. Flags 0x40 look like "user-editable" (Actual Start, Fixed Cost, Task ID have it; Earliest Start, Slack, Resource Cost don't) — guess. The low nibble picks a related object: 3 for the calendar (Work Days/Hours per Week), 4 and 5 for a dependency's predecessor and successor (From/To Name, From/To Task ID).
- Names:
ATNM128 (114 default names), 129 (user fields 0x1001 "Subtitle", 0x1002 "Attached Document") and 130 (0x54 "Note"). Documents copy them intoanamrecords; all 23 projects carry the same names (six use older wording, e.g. "Type" for "Lag Type"). Ids 0x19, 0x4a, 0x4b have none. - Per-class field getters, vtable slot +0x8 (returns a pointer to the field's value):
CDataObject_45cc(outline row),_53dc(summary),_5738(task),_2d1c(dependency),_0500/_0000(allocation),_3938(resource),_0a0c(calendar),_5dec(outline level),_0b64(cost bucket),_3560(note). They agree with the table offsets and add the references. - Type values (field 0x16, the class constant at vptr+0x78, stored as the outline/task "class" short), from STR# 11131: 0 Outline, 1 Custom Summary, 2 Outline Summary, 3 Palette (
outl/smry), 4 Task, 5 Milestone, 6 Supertask (task), 7 External (extk). The trailing string inCReadWrite_6348belongs to Supertasks.
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:
- Task (
outl/smry/task): +0x3c parent, +0x40 first child, +0x44 next sibling, +0x48 previous sibling (all 604 sibling links are mutual; a first child never has +0x48). +0xcc allocations (eachiall+0xa points back), +0x34 user-field values (uatt+0x34 = owner, +0x3c = field id), +0xd8 dependencies where the task is the predecessor, +0xdc where it is the successor (785 dependencies, each stored in both tasks), +0x30 Timeline shape (else the document's default per class,dcst+0xda/+0xde/+0xe2/+0xe6), +0xd4 calendar, +0xe0 subproject, +0x38 note, +0x50 text style, +0x4c per-field text styles (atstitems always point attxst), +0xfc / +0xfe the allocation whose duration overrides Duration / Actual Duration, +0xba the task's own actual duration (field 0x19). - Dependency: +0x2c predecessor, +0x28 successor (from the getter: From Name reads +0x2c), +0x38 calendar, +0x44 Lag, +0x4a Lag Type.
- Allocation: +0x6 resource, +0xa task, +0x4c bit 0x10 Driving Resource.
- Resource: +0x12 calendar. Outline level: +0x6 Level Name.
- Document (
dcst) +0xd2 is the project summary block (Tasks, Milestones, Done Tasks, dates, costs, counts); it matches the record counts (Business Start-up: 39 tasks + 3 milestones = 42task, 4smry, 3rsrc, 1cal).
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:
- Preferences (
CPreferencesDlg, views 3400–3408) →dcst: Document panel (Track Tasks in Open Charts, Page Breaks in Schedule Chart, Date and Duration Icons, Marks in Non-editable Table Cells, Non-scrolling Divider, Save Consolidated Master Dates; Default Calendar = +0xd6, Measurement = +0x228), Critical Path (threshold +0x230: 0 most critical tasks, 1 most critical path, 2 custom; custom days +0x21c; Show/Color/Size/Font/Style bits; its text style is atxstwhose index is at +0xf6), Resources (the leveling and gathering options), Clean-Up (Height/Width/Align to Grid; +0x204 is the default task size (height, width) and +0x208 the grid spacing (v, h) in pixels: (50, 86) and (10, 10) show as .69 × 1.19 in. and .14 in. in MacProject Pro itself), Timeline display (Slack/Planned/Actual/Baseline, Task Names, Sweep Actuals), Outline (Roll Up Costs, Line Labels, numbering at +0x10e: 0 numbers, 1 bullets, 3 inherited). Exact bits are inMPXKit's settings import (Project.swift). - Formats and Number panels → the 8 bytes at
dcst+0x8c: date order and format (popup indexes), date separator, flags (0x80 year, 0x40 month, 0x20 day, 0x10 time shown; 0x08 set = cents hidden), decimal and thousands separators, then ':' (guess: time separator). - Perspectives (
CPerspectiveDlg, views 4000/4005): the ten "Remember In This Perspective" boxes areprsp+0x166/+0x167 bits (defaults matchCConstruct_1168). +0xa2 is the Perspective's name, +0x114 its search formula (nestedsrch, e.g. Type = "Milestone"), +0x118 its sort order. - Column layouts: lists of {field id, width, shown}. The document has eight (+0x138…+0x154), Perspectives twelve (+0x11c…+0x148, by chart number). Named by their columns: chart 1 Outline, 2 Resource Timeline, 3 Task Timeline, 4 Resource Table, 5 Project Table, 7 Allocation Table, 8 Cash Flow Table, 9 Dependency Table (charts 0, 6, 10, 11 have none; guess Schedule Chart, Resource Histogram, Project Overview). The Chart menu (MENU 4) lists the same charts.
- Chart windows (
dcst+0x168,prsp+0x14c): per chart number, a window rect and a zoom percent (code 7, an IEEE double: 100.0 in every sample). - Page setup (
dcst+0x16c,prsp+0x150,FUN_000ba774): margins (code 0x1e; 54 = 0.75 in. at 72 dpi, guess), a 120-byteTPrintprint record (code 0 handle), then two optional panes, header and footer (order a guess; the first holds header text). - File references (
FUN_000bc230, insubp/supr/scop/edoc/extk/prsp): file name (+0x0), volume name (+0x44), directory ID (+0x84): e.g. "Prototype Subproject" on "King’sCourt". A subproject's +0x98 is its supertask in the master, and its four shorts +0x9c–+0xa2 are the indexes of itsMPB1–MPB4blocks (128/129, or −1 when absent; they match the blocks in Product Development Plan and Prototype Subproject). Calendars (cal+0x14) reuse the block, but there it holds a calendar name ("Project Calendar", "Editor's Calendar") and an empty volume; +0xa6 is usually the same name. +0xa6 is the name the app shows (Preferences and Task Info show "Default Calendar" where +0x14 holds "Project Calendar").
Checked in MacProject Pro itself (2026-10-17, a copy of Business Start-up):
- Preferences: every control on the Document, Critical Path, Resources, Clean Up, Timeline, Outline, Date and Number panels shows what the decoded fields and bits say (e.g. the critical path is drawn bold red:
txst#2, face 1, size 9, color 0xDDDD). - Task Info ("Idea Generation and Refining"): Task Duration 6 weeks (+0x74 = 864,000 s), Calendar Name, % Done 100, Leveling Priority 100, Outline Number 2.1; its allocation Owner (
rsrc#2), Work 6, Number 1.00, % Effort 100, Duration 6; no constraints; costs 0.00; Actual Start/Finish 1/1/93 and 2/11/93; custom field Subtitle. - Schedule Chart: a task's Location (+0x8) is the box's top-left (v, h) and Size (+0x10) its (height, width) in chart pixels: 50 × 86, the default task size; milestones 50 × 80. +0x18 (239, 181, 339, 364 for this task) looks like the box plus its date labels (guess).
- Schedule Chart drawing (Product Development Plan, Business Start-up, Layout ▸ Display Attributes):
dcst"fields" (16 field ids) are the Display Attributes: four slots per corner, in the order top-left, top-right, bottom-left, bottom-right. The first slot of each corner sits next to the box, and the next one stacks away from it. ES = slot 0, EF = slot 4, Duration = slot 12. Left-corner text ends about 8 px inside the box's left edge; right-corner text starts about 8 px inside its right edge. Labels have an opaque white background. A "Resource" slot shows an allocated resource (guess: the next one per slot, as Conference Planning's two Resource slots suggest).- Boxes are white with a 2 px shadow. Critical boxes have a 2 px frame. The name is top-aligned and centred. The dot fill marks 100% done tasks, not critical ones: "Research Market and Industry" is critical, 50% done, and plain.
- Notes: +0x18 is the note's absolute bounds on the chart. Attached notes (+0x36 set; +0x8 is relative) are drawn there, filled with +0x60 (gray 0xDDDD in Product Development Plan), with a black frame and a turned-down bottom-left corner. Free text notes are white and unframed. The +0x60 yellow (0xFFFF 0xFFFF 0x6666) is not drawn for them. A line whose top falls inside the note draws even if it overruns the bottom.
- +0x34 = 1 marks a legend (Layout ▸ Change to Legend). Its picture (a sample box) is drawn at +0x8/+0x10, and the Display Attributes field names are set around it in bold. Its +0x18 bounds include those labels.
- Pictures are stretched to +0x8/+0x10. The coloured phase bands in Product Development Plan are 39 × 480 pictures (a 50% dither) stretched up to 40 × 3809. Their patterns stay 8 × 8 screen pixels, as QuickDraw patterns do.
- Notes are drawn behind dependency lines and boxes. The first note in the list is frontmost (guess: the phase labels, notes 0–3, sit on the bands, notes 8–14). Attached notes sit above the other notes: the callout, note 17, which would otherwise be first drawn, shows on the yellow band.
- Perspectives: +0x9e is the View-menu command key (⌘1…⌘7 for the seven in Business Start-up). +0xa0 runs −2058…−2064 across them (guess: the menu command number). Edit Perspective shows the Remember boxes as decoded (Budget Reports: Search Formula off, the other nine on), and its search formula popup shows the referenced
srchrecord's name. - Search formulas (
srchterms): +2 the field, +4 the value, +0 the comparison, +0x11 how the term joins the next. Business Start-up's four searches (Milestones, Completed, In Progress, Critical) find exactly what their names say with: comparison 0 = is, 2 = less than, 4 = greater than; join 1 = And (QueryTests). Guesses: 1 = is not, 3/5 the or-equal forms, 6 = contains, join 2 = Or, and +0xe set meaning the value is another field. - A
txstfont of 0x8001 shows as Geneva,fontrecord #0 (guess: 0x8000 + a 1-based index into thefontrecords). - Durations show in weeks in this document ("Work-Weeks", the custom threshold's "Weeks"). The unit is
dcst+0x10a, set by Chart ▸ Duration Scale (CDurationScaleDlg, view 18000): 1 minute, 2 hour, 3 day, 4 week, 5 month in radio order; Business Start-up has 4, most samples 3.
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.