MacProject Pro 1.5 resource leveler
This page is docs/leveling.md from the repository, published as written.
Spec for Analyze ▸ Level Resources (⌘L), Unlevel Resources and View Leveler Results, read from the original's code and resources. No sample was leveled through the full path, so the code is the main evidence. One sample (Product Design Subproject) carries the result of a leveling substitution, which confirms the stored fields.
Markers:
- [code]: read from the decompiled code, with addresses.
- [resource]: read from a resource.
- [data]: seen in the samples.
- guess: inferred.
Segments: CLeveler 0xe2f00–0xe7a50, CLevelerDlg 0x120600–0x122138, CLevelResults 0x122200–0x123510, CXProjectLeveler 0x123600–0x123f14. Flag labels follow MPXKit's decoders (see file-format.md). A flag label's suffix maps to a bit mask: .a = 0x80, .b = 0x40, .c = 0x20, .d = 0x10, .e = 0x08, .f = 0x04, .10 = 0x02, .11 = 0x01. This is checked against +0x112.c = 0x20 in the scheduler notes, and against the code below.
1. Commands and settings
| Command | Number | Enabled when | Runs |
|---|---|---|---|
| Level Resources ⌘L | 1046 | always | CLeveler_3e60 |
| Unlevel Resources | 1047 | dcst +0x23e.b set (the "leveled" flag), or, with Cross-Project Leveling on, a family member qualifies |
CLeveler_488c |
| View Leveler Results | 1048 | always | opens window kind 0xb (view 1012, TLevelChangesWindow) |
All three are [code]: menu setup at 0x7102c–0x710a0, dispatch at 0x5b040–0x5b180.
Document settings (dcst; Preferences ▸ Resources, view 3403). The labels are [resource] and the masks [code]:
| Field | Meaning | Samples [data] |
|---|---|---|
| +0x23d.b (0x40) | Level During Calculate | off in all 23 |
| +0x23d.d (0x10) | Interactive Leveling: ask at each conflict | on in all 23 |
| +0x23d.e (0x08) | Cross-Project Leveling (project family) | off |
| +0x23f.b (0x40) | Allow Changes Only to Task Starting Dates | off |
| +0x23e.b (0x40) | the document has been leveled. Set by every applied change; cleared by Unlevel; enables Unlevel | set in Product Design Subproject only |
| +0x23e.f (0x04) | guess: the schedule needs recalculating. With it set and Level During Calculate off, leveling recalculates first |
The resource fields:
| Field | Meaning | Status |
|---|---|---|
| +0x44.40 (0x40) | Level: only these resources take part. In every one of 331 sample resources | [code] FUN_000e6374; [data] |
| +0x20 | # Available (float) | |
| +0x1c | in memory only: the leveler's segment list |
The task fields:
| Field | Meaning | Status |
|---|---|---|
| +0x100 | Leveling Priority. 0 is "Do Not Level"; 100 is "Highest" (Task Info label) | |
| +0xa2 | Early Start Type: bit set, below | |
| +0xa4 | Actual Start Type: same bits | |
| +0xa5 | Actual Finish Type: same bits | |
| +0xe4 | the Earliest Start before leveling | [code] |
| +0xe8 | the Actual Start before leveling | [code] |
Start-type bits:
| Bit | Name |
|---|---|
| 0x01 | User set |
| 0x02 | Leveler set |
| 0x04 | Swept |
| 0x08 | Bad |
| 0x10 | Constrained |
They are [resource]: STR# 11137, a 32-entry table indexed by the value. The table shows "Leveler set" over "User set" over "Swept", with a "Bad " prefix for 0x08, and "Constrained" only when it is the only bit set. In the samples, 13 tasks hold 0x10 (the constrained start milestones) and 606 hold 0 [data].
Allocation fields (iall). They are all serialized, so Unlevel works after the file is saved and reopened:
| Field | Meaning | Status |
|---|---|---|
| +0x2e | the resource before a substitution | [code] FUN_000e72cc; [data] |
| +0x3a | the Number before leveling (float) | [code] |
| +0x4a | the % Effort before leveling | [code] |
| +0x4c.a (0x80) | the leveler changed the effort | [code] CLeveler_0608 |
| +0x4c.b (0x40) | the leveler changed the number | [code] |
| +0x4c.c (0x20) | the leveler substituted the resource | [code] |
More on these:
- +0x2e: in Product Design Subproject, "Process Considerations" has resource 11, +0x2e = resource 29, and +0x4c.c set.
- +0x3a and +0x4a: in all 902 sample allocations they equal the current values (+0x1a, +0x1e) [data]. So write them as copies, and change only the current value when leveling.
- +0x4c.d (0x10): the driving-resource flag (already in file-format.md).
Leveling Priority names (STR# 6289) [resource]: Do Not Level, Lowest, Very Low, Lower, Low, Medium, High, Higher, Very High, Highest. How the numbers 0–100 map to these names: guess.
2. Who takes part
- A resource takes part if its Level flag is set (+0x44 & 0x40) [code
FUN_000e6374, called fromFUN_000e6730]. - A task is fixed — never moved, its allocations never changed — if any of these hold [code
FUN_000e3d84]:- % Done (+0xc6) ≥ 1 (it has started);
- Leveling Priority (+0x100) = 0;
- it is a supertask (class 6).
- Its allocations still count toward the load.
- The critical path gets no special treatment. It enters only through the ranking (§5), as lateness past the Latest Finish.
- An allocation is "clamped" when its Number > the resource's # Available or its % Effort > 100. The load then uses the clamped values. Clamped allocations, allocations of fixed tasks, and every allocation when Allow Changes Only to Task Starting Dates is on are marked rate-fixed: no effort or number change is offered for them [code
FUN_000e2ff8,FUN_000e47a0].
3. Load and conflicts
All times are seconds (the MacDate axis).
Load profile. Each leveled resource keeps a chronological chain of segments. A segment holds start (+0x1c), end (+0x20), load (+0x24, float), an "ignored" flag (+0x28), and the list of contributions in it [code
FUN_000e44cc,FUN_000e46f4,FUN_000e47a0].Contribution span. Each allocation of a task overlapping the window contributes over [task Earliest Start, allocation end). The allocation end is the task start plus the allocation's Duration (+0x46), worked on the task's calendar [code
FUN_000e30dc=FUN_000e3ce4(task, FUN_000e308c(alloc)); the calendar used is a guess].Segments split at every contribution boundary [code
FUN_000e46f4]. So there is no fixed time granularity: the profile is exact, event to event.Load of a contribution = min(Number, # Available) × min(% Effort, 100) [code
FUN_000e2ff8; SANE extended arithmetic]. It is in "percent of one unit": one person at 100% contributes 100.Capacity of a resource = # Available × 100.0 (the extended constant 100.0 at 0xe31c6 / 0xe6478) [code
FUN_000e313c,FUN_000e63d4].A conflict is a segment, not ignored, whose load > capacity, strictly (FOCPX then
ble) [codeFUN_000e63d4].- The leveler takes the earliest-starting conflict across all leveled resources.
- On a tie, the first resource in the resource list wins (strict
<) [codeFUN_000e6484].
Contribution record [code
FUN_000e47a0]:Offset Contents [0] fixed (task fixed) [1] rate-fixed [2] working "kept" mark [4] load (float) [8] allocation end [0xc] cross-project flag [0xe] allocation reference [0x12] task Leveling Priority
4. The main loop (CLeveler_3e60 @ 0xe6d64, FUN_000e64e8 @ 0xe64e8) [code]
- If Level During Calculate is off and +0x23e.f is set, calculate the schedule first (
CAnalysis_0080). - Find the overall range: the earliest task start to the latest end, over tasks and gathered allocations (
FUN_000e6ca0). Process it as one window. On a memory-full failure (−108) the window is halved, up to 5 times, and retried. - Without Interactive Leveling, show a progress window titled "Leveling resources" (STR 15003) with the conflict's date. Cancelling stops.
- Gather the load profile for the window (
FUN_000e6a10→FUN_000e6814over tasks,FUN_000e69bcover resources). - Repeat until there are no conflicts, or Stop:
- Take the next conflict (§3).
- Build its solutions (§5): at most 3, sorted best first.
- Choose one:
- Not interactive: the first solution, applied automatically.
- Interactive: the Interactive Leveling™ dialog (view 11000,
CLevelerDlg_0d00). It shows the conflict text, up to four solutions as radio buttons each with a before/after picture (TSolutionPict), and the solution text. Its buttons are Make Changes, Ignore Conflict and Stop Leveling.
- If there's no solution, or the user chose Ignore Conflict: mark the segment ignored (+0x28). Rescan from its end later (re-gather once the scan passes it).
- Otherwise:
- record the conflict and the solution for the results (
FUN_000e4c3c); - truncate the profiles at the conflict start (
FUN_000e6bc0); - apply the solution (§6);
- recalculate the schedule (
CAnalysis_07dc, which honours leveled starts, §7); - re-gather the profile from the conflict start to the window end.
- record the conflict and the solution for the results (
- Mark the document changed. Without Cross-Project Leveling, run a final calculation and a redraw (
CAnalysis_0460). - If any changes were recorded, show the results (
CLevelerDlg_0e38).
5. Solutions for one conflict (FUN_000e504c, FUN_000e6010, FUN_000e5e78, FUN_000e5b78) [code]
The conflict context holds:
- capacity = # Available × 100;
- n, the number of contributions;
- the number of movable contributions (not fixed);
- the number of rate-changeable contributions (not rate-fixed);
- maxPriority, the highest Leveling Priority in the segment (
FUN_000e5e28).
There are three kinds of solution. Each is a list of changes: per allocation, none, substitute (type 1), move (type 2), or effort or number change (types 3 and 4).
Moves
The leveler enumerates the sets K of contributions that stay where they are.
- Fixed contributions are always in K.
- Contributions are added to K while Σ load(K) ≤ capacity.
- Only maximal sets count: no further contribution would fit.
- The search keeps the set with the least leftover capacity (capacity − Σ load). An exact fit (leftover = 0) is taken at once.
- The recursion is
FUN_000e5b78; finding the next contribution that fits isFUN_000e5af8. - If at most one contribution is movable and it is the one being considered, it is skipped [
FUN_000e5e78].
For a set K, every movable contribution not in K is moved (type 2) to moveDate, computed by FUN_000e5a64 / FUN_000e5958:
moveDate= max(m, x).- m = the earliest allocation end among the contributions in K and the fixed ones. It is when the first staying allocation frees the resource.
- x = for each task in K, the latest end of its allocations that also sit in this conflict's segment for another resource (
FUN_000e58e8/FUN_000e5868). Exact scope: guess.
The moved task's new Earliest Start = CAnalysis_1314(task, moveDate, flag), which is the scheduler's start_at(W(moveDate)): the next working start on the task's calendar [CLeveler_0568]. flag is set when the task has no predecessor or no successor list (+0xdc / +0xde = −1) and controls whether a zero-duration task stays on the instant.
Effort or number changes
These are only for rate-changeable contributions, so never when Allow Changes Only to Task Starting Dates is on.
FUN_000e51d0(ctx, k) gives k overlapping allocations an equal share:
- k ≤ 5: each gets % Effort = 100 ÷ k (integer division) and Number = min(Number, # Available). The share is # Available × 100 ÷ k.
- k ≥ 6: a small search for a number and an effort of 100 ÷ s (s = 2…5) that fit.
FUN_000e537c then:
- writes the new Number and/or % Effort for each chosen allocation whose load exceeds the share. It's type 3; type 4 when the allocation already ends at the segment start (guess);
- recomputes that allocation's end with the lower rate: same work, longer duration (
CLeveler_0514→FUN_000e31d0).
It combines up to depth allocations, where depth comes from n:
| n | depth |
|---|---|
| < 8 | n − 1 |
| 8 | 4 |
| 9–10 | 3 |
| 11–19 | 2 |
| ≥ 20 | 1 |
Substitution
Only with Interactive Leveling on, Allow Changes Only to Task Starting Dates off, and more than one resource in the document. One "Substitute for FUN_000e4e0c). Choosing it opens Resource Substitution (view 11001): a "Substitute For" resource list and a task list. This is the dialog's fourth radio button (guess).
Ranking
Solutions are kept in a sorted array whose class overrides Compare with CLeveler_1314 @ 0xe4218. Only the best 3 are kept (FUN_000e5650).
Each solution carries a score, filled in by FUN_000e4194 / FUN_000e4070. For each changed allocation, CLeveler_0360 gives f, the new finish of its task after the change, and LF, the task's Latest Finish (+0xb6). The score fields:
| Field | Meaning |
|---|---|
| +0x14 | total load of the unchanged contributions |
| +0x18 | moved tasks whose priority < maxPriority |
| +0x1a | Σ (f − LF) over changed tasks at maxPriority |
| +0x1e | Σ (f − LF) over all changed tasks |
| +0x22 | Σ max(0, LF − f) at maxPriority, the slack left |
| +0x26 | Σ max(0, LF − f), all |
| +0x2a | the number of changes |
| +0x2c | moved elements carrying mark [2]; exact meaning guess |
The order, best first:
- Move or combined solutions before a lone substitution (a single type-1 change).
- Fewer +0x2c.
- A comparison of +0x14 against the other solution and the capacity (SANE branches): prefer the solution that fills capacity exactly. Details: guess.
- More low-priority tasks moved (+0x18): lower-priority tasks are moved first.
- Less lateness past LF for the highest-priority tasks (+0x1a), then for all tasks (+0x1e).
- More slack left for the highest-priority tasks (+0x22), then for all (+0x26).
- The +0x14 comparison again, then fewer changes (+0x2a).
So by default the leveler delays the lowest-priority tasks, choosing the moves that push tasks least past their Latest Finish and keep the most slack. Moves are not limited to slack: lateness is only penalised.
6. Applying a change (CLeveler_0608 @ 0xe350c, an iall method) [code]
- Move, planned mode (
dcst+0x112.c off):- save the task's ES into +0xe4;
- Early Start Type (field 112, +0xa2) |= 0x02;
- Earliest Start (field 8) = the new date;
- Actual Start Type (field 113) = +0xa4 & ~0x04.
- Move, actuals mode:
- save +0x5c into +0xe8;
- Actual Start (field 13) = the new date;
- Actual Start Type = (+0xa4 & ~0x04) | 0x02.
- Effort or number change:
- if the effort changed: % Effort (field 79) = the new effort, and +0x4c |= 0x80;
- if the number changed: Number (field 78) = the new number, and +0x4c |= 0x40.
- Substitution: Resource (field 52) = the new resource, and +0x4c |= 0x20.
- After any change:
- the task is marked for recalculation (
CAnalysis_1590); dcst+0x23e |= 0x40 (leveled);- with Cross-Project Leveling on, also +0x23e |= 0x80.
- the task is marked for recalculation (
7. How the scheduler uses a leveled start [code FUN_0004812c @ 0x4812c]
When the Early Start Type has 0x02 (Leveler set), the forward pass pins the task's start to its stored Earliest Start (+0x54). That overrides both the predecessors' bound and the start constraint, like Start On. Otherwise the constraint rules in scheduling.md apply.
"Bad leveler set" (0x08 | 0x02) is presumably shown when a pinned start falls before what the dependencies require. Guess: the code that sets 0x08 wasn't traced.
The Swift scheduler must add this rule. In the samples no task has 0x02, so the 618 of 619 match is unaffected.
8. Unlevel (CLeveler_488c @ 0xe7790, FUN_000e7388, FUN_000e72cc) [code]
For every task that isn't fixed:
- If Early Start Type has 0x02:
- Earliest Start (field 8) ← +0xe4;
- Early Start Type ← clear 0x02, and also 0x10 if it was set.
- If Actual Start Type has 0x02:
- Actual Start (field 13) ← +0xe8;
- Actual Start Type ← clear 0x02 (and 0x10).
- For each allocation:
- if +0x4c.c is set and +0x2e is a valid reference: Resource ← +0x2e, clear 0x20;
- if +0x4c.a: % Effort ← +0x4a, clear 0x80;
- if +0x4c.b: Number ← +0x3a, clear 0x40.
If anything changed, recalculate with Level During Calculate temporarily off. In every case, clear dcst +0x23e.b (leveled).
With Cross-Project Leveling, the same is done in each family file (FUN_000e7604, FUN_000e7520).
9. View Leveler Results (CLevelResults, view 1012 TLevelChangesWindow / TLevelChangesView)
A scrolling list of what the last leveling run did. The message templates are [resource]: STR 11000–11016. The joiners are ", " (11001), " and " (11000), "; " (11004) and "." (11002).
Per conflict:
- "^2 is over-allocated between ^0 and ^1. The tasks are ^3" (11003): ^2 is the resource, ^0 and ^1 the conflict's start and end, ^3 the task names.
Then its changes, joined by "; ":
| Change | Template |
|---|---|
| move | "move start of ^4 to ^1" (11006) |
| effort down | "decrease effort from ^0 to ^1 in ^4" (11007) |
| effort up | "increase effort from ^0 to ^1 in task ^4" (11008) |
| number down | "decrease number from ^2 to ^3 in task ^4" (11009) |
| both | 11010–11014 |
| substitution | "Substitute for ^0…" / "Substitute For ^0" (11005, 11016) |
| ellipsis | "…" (11015) |
The records are added with CDocumentDataManager_28ac (CLeveler_0b8c): kind 2 a planned move, 1 an actual move, 3 an allocation change, plus the date or rate and the resource and task. They appear to live in a document list, dcst +0x158, which Unlevel clears (guess). Whether they are saved: guess, probably not.
10. What the editor needs to decide
- Interactive Leveling is on in every sample. So the original's default experience is the per-conflict dialog: Make Changes / Ignore Conflict / Stop Leveling with up to 4 solutions. Implement the dialog, or default to automatic (the first solution) and offer the dialog behind the preference.
- Substitution needs the Resource Substitution dialog. It can follow later; the other solutions don't depend on it.
- The scheduler must honour Early Start Type 0x02 (§7) before leveling can work. Unlevel needs +0xe4/+0xe8 and the allocation originals; the model should carry them, and the MPRX writer should round-trip them.
- The +0x14 tie-break and the
xbound on moveDate are only partly decoded (guesses). A port that skips them still follows the main order: low priority first, then least lateness, then most slack, then fewest changes.
Where things are
- Command numbers:
mntb128, Analyze items 14–16 = 1046–1048. - Vtables:
- the solutions array is the vtable at A5 −0x1300 → 0xffffb770, whose Compare slot is
CLeveler_1314; - the allocation class is vtable 0xffffaecc, with slots +0x34…+0x54 =
CLeveler_0360…089c. - Calls go through table − 4, so a call offset is the slot + 4.
- the solutions array is the vtable at A5 −0x1300 → 0xffffb770, whose Compare slot is
- SANE opcodes: source format 0x0000 extended, 0x1000 single, 0x2000 integer. Operations: 0x00 add, 0x02 sub, 0x04 mul, 0x06 div, 0x08 cmp, 0x0A cmpx, 0x0E to extended, 0x10 from extended.