MacProject Pro 1.5 scheduling rules
This page is docs/scheduling.md from the repository, published as written.
How MacProject Pro 1.5 calculates a schedule, worked out from the dates it stored in its 31 sample files and checked against its code. MPXKit's Scheduler implements these rules; SchedulerTests recomputes every sample and compares with the stored dates.
Result: 618 of 619 sample tasks match on all six stored fields (ES, EF, LS, LF, slack, critical). Summaries match on ES and LS (28 of 28) and on EF, LF and duration (27 of 28). The exceptions are under "Remaining mismatches".
Labels:
- [data] means confirmed by the samples.
- [code] means confirmed in the decompiled
CAnalysissegment (0x44c00–0x4acc0). - guess means unconfirmed.
Fields
Task (task, and extk by the same layout):
| Field | Offset |
|---|---|
| Earliest Start | +0x54 |
| Earliest Finish | +0x58 |
| Actual Start | +0x5c |
| Actual Finish | +0x60 |
| Start Constraint | +0x6c |
| Finish Constraint | +0x70 |
| Task Duration (work seconds) | +0x74 |
| Start/Finish Constraint Type | +0xa0 / +0xa1 |
| Actual Start/Finish Type (non-zero = entered) | +0xa4 / +0xa5 |
| Latest Start | +0xb2 |
| Latest Finish | +0xb6 |
| actual duration | +0xba |
| Slack (work seconds) | +0xc2 |
| % Done | +0xc6 |
| allocations | +0xcc |
| calendar | +0xd4 |
| successor deps | +0xd8 |
| predecessor deps | +0xdc |
| the allocation driving Duration (-1 = none) | +0xfc |
| Critical | +0x106.11 |
Dependency (idep):
| Field | Offset |
|---|---|
| successor | +0x28 |
| predecessor | +0x2c |
| lag calendar | +0x38 |
| Lag (work seconds) | +0x44 |
| Lag Type | +0x4a |
Allocation (iall):
| Field | Offset |
|---|---|
| resource | +0x6 |
| Duration (work seconds on the resource's calendar) | +0x46 |
| Driving Resource flag | +0x4c.d |
The resource's calendar is rsrc +0x12.
Document (dcst):
| Field | Offset |
|---|---|
| project start | +0x246 |
| supertask start/finish in the master, for subprojects | +0x20c/+0x210, +0x214/+0x218 |
| critical-path threshold mode | +0x230 |
| custom threshold (work seconds) | +0x21c |
| schedule from actuals (bit 0x20 of byte +0x112) | +0x112.c |
Calendars [data]
A cal record holds one week entry per weekday, Sunday first. Each entry is a 48-bit mask, first slot, last slot, slot count, working seconds, and a working-day flag:
- Slots are half hours. Bit 47 is 00:00–00:30, so slot s covers s·30 min to (s+1)·30 min.
- Consecutive set slots form the working intervals. A lunch break is a gap, as in Package Design's 9–12 and 13–18 week (
00003f3ff000). - A day whose flag is 0 doesn't work.
+0xe6 days lists exceptions as (work seconds from 1904-01-01 to that date, date, working seconds that day).
- A 0 is a holiday, for example 1993-03-17 in the Lessons. The first value is a cache: work seconds from 1904-01-01 to that date. You can rebuild it.
- What a non-zero exception means is a guess (that day works that many seconds). No sample has one.
Fields that don't affect dates:
+0x6,+0xa,+0xeare seconds per week, day and month. They set the duration display units.+0xea/+0xecare a year and month (guess: the month the calendar window shows).
Time arithmetic [data]
All scheduling runs on a work-time axis. W(t) is the number of working seconds from an epoch to instant t on a given calendar. Two conversions go back from a position w to an instant:
- start_at(w): the latest instant with W = w. It is where work resumes, e.g. 09:00 of the next working day.
- finish_at(w): the earliest instant with W = w. It is where work stopped, e.g. 17:00 (18:00 on a 9–18 calendar).
How a task with duration d lands:
- Forward: ES = start_at(W(bound)) and EF = finish_at(W(ES) + d). A 3-day task from Wed 7/1 09:00 finishes Fri 7/3 17:00, not Mon 09:00.
- Backward: LF = finish_at(W(bound)) and LS = start_at(W(LF) − d).
Slack is W(LF) − W(finish) in work seconds, on the task's calendar. The finish is EF, or the Actual Finish when actuals drive (see below).
A task uses its own calendar (+0xd4); the default is the first cal . A lag is work time on the dependency's calendar (idep +0x38; every sample uses calendar 0, so the separate lag calendar is a guess).
Forward pass (topological order)
The bound t0 is an instant: the latest of the predecessors' terms.
- Lag type 0, Finish to Start: the predecessor's finish, plus the lag (finish_at(W(finish) + lag)). [data]
- Lag type 1, Start to Start: the predecessor's ES, plus the lag. [data] (Community Service: Hatch Chicks 9/9, plus 15 days, gives 9/30.) Those are the only two types (Task Info's Dependency radio buttons).
A task with no predecessors starts at the project start, dcst +0x246 (6/1/92 in the templates, 1/25/93 in the tutorials). [data]
Start constraints apply next [code, FUN_00048b14].
- Type 1, Start On, sets t0 to the date.
- Type 2, Start No Earlier Than, takes the later of the two.
- Type 3, Start No Later Than, takes the earlier.
- Type 4, Early Start, is "as soon as possible" and has no date.
- The Task Info popup lists the names in that order. The samples use only type 1, on start milestones, and type 4 without a date.
Dates:
Zero-duration tasks (milestones and class-4 tasks of duration 0) sit on the bound instant: ES = EF = t0, so they are at 17:00 after a task that ends then. With no predecessor and no constraint, ES = EF = start_at(W(project start)). [data]
Other tasks go through each lane and take the earliest start and latest finish over the lanes:
- the task on its own calendar with its duration;
- each allocation whose resource uses a different calendar, with the allocation's Duration (+0x46) on that calendar.
For each lane, s = start_at(W(t0)) and f = finish_at(W(s) + d). [data] This is why "Business Feasibility Study" starts at 07:00 (Operations Manager, 7–16 calendar), and a Statistician on an 8–12 calendar makes "Market Research" finish late.
Duration is the task's +0x74, unless a driving resource is set (+0xfc ≥ 0). Then it is that allocation's Duration (+0x46) [data]. Magazine Publishing's "Draft Copy" stores 8 days, but its driving allocation is 10 days, and the file shows 10.
Actuals [code FUN_00047f18/FUN_0004805c, data Lessons 6-11]:
- When the document's actuals switch is on (dcst +0x112 bit 0x20, label +0x112.c), each task's finish for successors and slack is its Actual Finish.
- It is the entered Actual Finish when +0xa5 is set.
- Otherwise it is the Actual Start plus the Actual Duration (+0xba). The Actual Start is the entered one when +0xa4 is set, else ES.
- ES and EF themselves stay the planned dates.
- When the switch is off, actuals and % Done don't move dates.
- Only Lessons 6-11 has the switch on. "Select Subcontractors" has an Actual Start of 3/2, which with 15 days and the 3/17 holiday gives an Actual Finish of 3/23, so slack is 1 day.
Backward pass (reverse order)
The bound t1 is an instant: the earliest of the successors' terms.
- Finish to Start: the successor's LS, minus the lag.
- Start to Start: it bounds this task's start. Use finish_at(W(succ LS − lag) + d) as the finish bound.
A task with no successors gets t1 = its own finish, so every chain's end has zero slack [code FUN_00048ca0: the pass starts unbounded; data: Software Development's "Translation Kit Released" ends 12/5 with LF 12/5, while the project runs to 1/2/95].
Finish constraints then apply, mirroring the start constraints on t1 (1 Finish On, 2 No Earlier Than, 3 No Later Than, 4 Late Finish). This is a guess; no sample has one.
Dates:
- Zero-duration tasks: LS = LF = t1, so they sit at 09:00 before a successor that starts then. [data]
- Other tasks: per lane, f = finish_at(W(t1)) and s = start_at(W(f) − d). LF is the latest f and LS the earliest s. [data] ("Market Research" gets LF 8/3 09:00 from its 8–12 Statistician.)
Critical [code FUN_00044fac/FUN_00044fe8, data]
A task is critical when slack ≤ limit. The limit depends on dcst +0x230:
- 0, Most Critical Tasks: the least slack of any task, floored at 0. With unbounded end tasks, that means slack ≤ 0.
- 2, Custom: dcst +0x21c (work seconds).
- 1, Most Critical Path: treated as mode 0 (guess).
Every sample uses mode 0.
Summaries [data]
- Custom summaries (
smry):- ES/LS are the earliest of their tasks' ES/LS.
- EF/LF are the latest of their tasks' EF/LF.
- Their tasks are all tasks beneath them, through nested outline rows.
- Duration (+0x74) is W(EF) − W(ES).
- Slack is the least slack among their tasks (26 of 28).
- Outline headings (
outl) store no dates. Compute them for display the same way.
Subprojects
- A supertask (class 6) is scheduled as a task with its stored duration [data].
- A subproject file saved while consolidated into its master starts from its supertask's start,
dcst+0x20c, not +0x246 [data: Package Design and Product Design Subprojects]. Prototype Subproject and Addition Subproject were saved standalone. The code applies the master's bounds only whenCDocumentDataManager_1948returns more than zero (guess: when the master is open). - The checker tries both start rules for files with real master bounds.
Writing a file back
These are the working copies the scheduler leaves [data]:
- +0xf4 = ES (619 of 619)
- +0xf0 = EF (618)
- +0xf8 = the finish (Actual Finish when actuals drive, else EF; 618)
- +0xec = ES (606; 13 constrained start milestones keep an older date)
- An Actual Start/Finish that wasn't entered holds ES/the finish (615 and 618).
Remaining mismatches
- Product Development Plan, "Packaging Design" (EF/LF 1/28 18:00 and LS 12/1 17:00): the supertask's finish comes from its subproject's consolidation (
MPBnrollup). The subproject runs on a 9–12/13–18 calendar and ends 1/28 at 18:00, while the master's calendar ends at 17:00. The odd LS (17:00 of the day before ES) looks like the master subtracting the duration from that 18:00 finish without normalizing to a start (guess). Reproducing it needs the subproject file or itsMPBnblock. - Summary "PHASE ONE" (Product Development Plan): it stores EF 11/30 and LF 12/1, while its six tasks end 10/28 and 10/29. Its stored duration agrees with its stored dates, so the summary looks stale, left from before tasks moved out of it.
- Two summary slacks (Business Start-up "Initial Preparation", Prototype Engineering "ANALYSIS"): they store 0, while the least slack of their tasks is 11 weeks (1,584,000 s) and 25 days (720,000 s). Unexplained (guess: stale, like PHASE ONE).
Not exercised by any sample: finish constraints, start constraints of types 2 and 3, critical-path modes 1 and 2, non-zero calendar exceptions, lag calendars other than calendar 0, resource leveling (Level During Calculate is off everywhere), and external tasks (extk, none in the samples).