# PLAN-LAZARUS-PORT — HC Launcher: Delphi → Lazarus/FPC One-Way Migration

**Status: COMPLETE** — Merged to trunk at r1066 (2026-04-13). Delphi files removed, `last-delphi` tag preserved.

## Context

HC Launcher was a single-form Delphi VCL application (1422 lines in `uMain1.pas`) that built command-line arguments for the Harpoon Classic Game Engine. Its UI was built entirely on TMS Component Pack controls (89 instances, ~2,258 TMS-specific DFM properties) which are commercial and Delphi-only.

**Why:** Lazarus/FPC is the target toolchain for the Stratsims ecosystem going forward. Delphi has been dropped.

**Outcome:** A Lazarus project using standard LCL controls, with the command-line builder extracted into its own interface+class, covered by 80 FPCUnit tests. High-DPI ready. Verified to launch the HC GE successfully.

---

## Decisions

- Replace all TMS controls with standard LCL equivalents (no TMS FNC)
- Drop ExeInfo — use existing `CICGetAppVersionStr` from `../shared/pascal/uCICVersionInformation.pas`
- Drop SE and Other tabs (empty stubs — will return later)
- One-way migration, no dual-codebase IFDEFs
- Extract `BuildCommandLineGE` into `uHCLCmdLineBuilder.pas` with interface + implementation

---

## Target File Layout

```
HCLauncher/
  Launcher.lpi                    ← new Lazarus project
  Launcher.lpr                    ← new program entry point
  Launcher.res                    ← reused
  uMain1.pas + uMain1.lfm        ← rewritten (LCL, no TMS)
  uHCLCmdLineBuilder.pas         ← NEW: extracted builder
  uHCLProcessUtils.pas           ← NEW: replaces FmxUtils.pas (license-incompatible)
  tests/
    HCLauncherTests.lpi + .lpr   ← FPCUnit test runner
    test_CmdLineBuilder.pas      ← unit tests for builder
    test_Settings.pas            ← INI round-trip tests
```

Legacy files (`Launcher.dpr`, `Launcher.dproj`, `uMain1.dfm`, `FmxUtils.pas`) stay in SVN history as reference. `FmxUtils.pas` is NOT carried forward due to licensing restrictions.

---

## Phase 1 — Infrastructure: Project Files + Utilities ✓ COMPLETE (r1061)

### 1.1 Create `Launcher.lpr` and `Launcher.lpi`

Program entry point using LCL `Interfaces` unit. `.lpi` configured with:
- Unit search path: `../shared/pascal`
- Output dir: `lib/$(TargetCPU)-$(TargetOS)` (add to `.svnignore`)
- Build modes: Release + Debug
- `GraphicApplication = True`

### 1.2 Create `uHCLProcessUtils.pas` (replaces `FmxUtils.pas`)

**File:** `C:\Prog\Projects\StratsimsOSS\HCLauncher\uHCLProcessUtils.pas` (new)

`FmxUtils.pas` has a license incompatible with our proprietary requirements and is dropped entirely. The only function HC Launcher actually calls from it is `ExecuteFile` (a thin `ShellExecute` wrapper used in `btnLaunchGEClick`). We rewrite only what we need:

```pascal
unit uHCLProcessUtils;

{$mode objfpc}{$H+}

/// <summary>
/// Process launch utilities for HC Launcher.
/// Replaces the subset of FmxUtils.pas that HC Launcher used.
/// </summary>
```

**Functions to implement:**

- `ExecuteFile(const AFileName, AParams, ADefaultDir: String; AShowCmd: Integer): Boolean`
  - `{$IFDEF MSWINDOWS}`: Uses `ShellExecute` from `ShellAPI` (same logic as original, ~5 lines)
  - `{$ELSE}`: Uses `TProcess` from FCL `Process` unit — sets `Executable`, `Parameters`, `CurrentDirectory`, calls `Execute`
  - Returns `Boolean` (success/failure) instead of `THandle` — the original return value was never checked by the caller

No other FmxUtils functions (`CopyFile`, `MoveFile`, `GetFileSize`, `FileDateTime`, `HasAttr`, `ExecuteFileEx`, `GetShortName`, `IsWow64`) are used by HC Launcher and are not carried forward. If needed later, they can be implemented fresh or sourced from `uCICExecuteProgram` in the shared units.

### 1.3 Update `uCICVersionInformation.pas`

**File:** `C:\Prog\Projects\StratsimsOSS\shared\pascal\uCICVersionInformation.pas`

- Add `{$IFDEF FPC}{$MODE DELPHI}{$ENDIF}` before `unit` keyword
- Change `uses` from `System.Types, System.SysUtils` to plain `SysUtils` (Delphi accepts both)
- Guard implementation body with `{$IFDEF MSWINDOWS}` — return `''` on other platforms
- **Risk:** Shared with sibling Delphi projects. Verify a sibling build still compiles after this change.

### 1.4 Create stub `uMain1.pas` + `uMain1.lfm`

Minimal `TfrmMain1 = class(TForm)` that compiles and opens a blank window.

**Acceptance:** `lazbuild Launcher.lpi` exits 0. App opens blank window and closes cleanly. No TMS units anywhere.

---

## Phase 2 — Extract `uHCLCmdLineBuilder` ✓ COMPLETE (r1061)

**File:** `C:\Prog\Projects\StratsimsOSS\HCLauncher\uHCLCmdLineBuilder.pas` (new)

This is the most important architectural change. Done before the form rewrite so tests serve as the specification.

### 2.1 `THCGECmdLineInputs` record

Flat record capturing every value that `BuildCommandLineGE` currently reads from controls. ~50 fields organized by flag group:

| Group | Fields | Flag |
|-------|--------|------|
| Log file | `LogFN: Boolean`, `LogFNPath: String` | `-f` |
| Logging | `LoggingEnabled`, `LogAll`, 18× individual log booleans | `-l` |
| Debug | `DebugEnabled`, `DebugI32501` | `-I` |
| Window hide | `WindowHideEnabled`, 5× sub-booleans | `-h` |
| Switches | `IgnoreReg`, `AutoPlay`, `SubAIDisable`, `Eff115Old` | `-i`, `-a`, `-x`, `-e` |
| Save interval | `SavIntEnabled`, `SavIntText` | `-t` |
| Iterative save | `IterSave` | `-s` |
| Iterative save GT | `IterSaveGT`, `Interval`, `Saves`, `FNEnabled`, `FNPath` | `-S`, `-k` |
| Message log | `LogMLFNEnabled`, `LogMLFNPath` | `-m` |
| Battle set | `BSByNum`, `BSNumIndex`, `BSByFile`, `BSFilePath` | `-b` |
| Scenario | `ScenByNum`, `ScenNumIndex`, `ScenByFile`, `ScenFilePath` | `-c` |
| Save file | `SavFNEnabled`, `SavFNPath` | `-v` |
| CD databases | `CDBresFNEnabled`/`Path`, `CDBrsrFNEnabled`/`Path` | `-d`, `-r` |
| Game options | `GameOptsEnabled`, `GOUseSideChecklist`, `GOSideIndex`, `GOSidesBitmask`, 8× option indices | `-g` |
| CPU affinity | `AffinityEnabled`, `AffinityIndex` | `-p` |
| Priority | `PriorityEnabled`, `PriorityIndex` | `-u` |

### 2.2 `IHCLCmdLineBuilder` interface + `THCLCmdLineBuilder` class

```pascal
IHCLCmdLineBuilder = interface
  function BuildCommandLine(const AInputs: THCGECmdLineInputs): String;
end;
```

Implementation is a direct extraction of the ~230-line `BuildCommandLineGE`, with these improvements:

**Constant arrays replace case statements:**
- `BSIndexToID: array[0..31]` — maps combo index to battle-set ID (skips ID 5; index 31→35)
- `AffinityMask: array[0..3]` = (1, 2, 4, 8)
- Priority = `Index - 2`
- Scenario ID = `Index + 1`

**Locale bug fix:** Replace `IsFloat(S)` (uses locale-dependent `StrToFloat`) with `TryStrToFloat(S, Val, InvariantFS)` where `InvariantFS.DecimalSeparator = '.'`.

**No UI dependency:** Only `uses SysUtils, Math`.

### 2.3 Tests: `tests/test_CmdLineBuilder.pas`

FPCUnit test suite (~35 test methods). Key coverage:

- Empty inputs → empty string
- Each flag independently (log file, individual `-l` flags, `-l all` short-circuit, debug, window hide, switches, intervals, battle sets, scenarios, game options, affinity, priority)
- Battle-set index mapping edge cases (index 4→ID 6 skip, index 31→ID 35)
- `-S` format verification: `'-S' + Format('%0.5d', [100]) + Format('%0.4d ', [5])` = `'-S001000005 '`
- `-g` 9-digit string with both old-GE checklist bitmask and new-GE combobox modes
- Locale fix: `'1,5'` rejected, `'1.5'` accepted for `-t`
- Flag ordering matches original (f, l, I, h, i, a, t, s, S, m, x, e, b, c, v, d, r, g, p, u)
- Full realistic input → verified complete string

### 2.4 Test runner: `tests/HCLauncherTests.lpi` + `.lpr`

Console FPCUnit runner (pattern: `../shared/pascal/tests/`).

**Acceptance:** `lazbuild tests/HCLauncherTests.lpi && tests/HCLauncherTests` — all builder tests pass. `uHCLCmdLineBuilder` has zero LCL imports.

---

## Phase 3 — Rewrite Form (`uMain1.pas` + `uMain1.lfm`) ✓ COMPLETE (r1061)

### 3.1 Build `.lfm` from scratch in Lazarus IDE

Do NOT convert the `.dfm` — it's ~8700 lines of TMS properties. Use the original as a layout reference only.

**TMS → LCL control map:**

| TMS (count) | LCL Replacement | API change |
|-------------|----------------|------------|
| TAdvSmoothTabPager + TabPage (1+3→1+1) | TPageControl + TTabSheet | Only GE tab |
| TAdvSmoothComboBox (13) | TComboBox | `.SelectedItemIndex` → `.ItemIndex`; `OnItemSelected` → `OnChange` |
| THTMLCheckBox (46) | TCheckBox | Same `.Checked` API |
| THTMLCheckList (1) | TCheckListBox | Same `.Checked[i]` API |
| TAdvFileNameEdit (8) | TEdit + TButton + TOpenDialog | `.FileName` → `.Text`; browse via dialog |
| TAdvDirectoryEdit (1) | TEdit + TButton + TSelectDirectoryDialog | Same pattern |
| TAdvEdit (2) | TEdit | Direct |
| TAdvSpinEdit (2) | TSpinEdit | Same `.Value` API |
| TAdvSmoothPanel (5) | TGroupBox | Visual container |
| TAdvSmoothButton (1) | TButton | Direct |
| THTMLabel (4) | TLabel | Strip HTML from captions |
| TAdvLUEdit (1) | TEdit | Direct |
| TAdvOfficeStatusBar (1) | TStatusBar | Direct |
| TExeInfo (1) | Dropped | Use `CICGetAppVersionStr` |

### 3.2 Rewrite `uMain1.pas`

```pascal
uses
  LCLIntf, LCLType, Classes, SysUtils, Forms, Controls, StdCtrls, ComCtrls,
  Dialogs, ExtCtrls, Spin, CheckLst, FileUtil, Registry, IniFiles, Math,
  uHCLProcessUtils, uCICVersionInformation, uHCLCmdLineBuilder;
```

**Key changes:**
- `FBuilder: IHCLCmdLineBuilder` created in `FormCreate`, freed automatically (interface ref-counted)
- New `SnapshotInputs: THCGECmdLineInputs` method reads all LCL controls into the record
- `BuildCommandLineGE` calls `FBuilder.BuildCommandLine(SnapshotInputs)` and sets `edtCmdLineGE.Text`
- `FormCreate`: version from `CICGetAppVersionStr(Application.ExeName)`, no ExeInfo
- `FormDestroy`: drop OLE `RevokeDragDrop` calls (no OLE in LCL)
- `SettingsLoad`/`SettingsSave`: same INI keys, `.SelectedItemIndex` → `.ItemIndex`, `.FileName` → `.Text`
- Version-gating in `edtGENameChange`: unchanged logic, same `CICGetAppVersionStr` + string comparisons
- Mutual-exclusion logic (BS num/file, Scen num/file, debug/WH cascading) stays on the form

### 3.3 Settings extraction for testability

Extract `SettingsLoad`/`SettingsSave` to accept a record + `TIniFile`, making them testable without a form instance. Form methods become thin wrappers. Complete `tests/test_Settings.pas` with INI round-trip tests.

**Acceptance:**
- `lazbuild Launcher.lpi` in Release and Debug
- App loads existing `Launcher.ini` correctly
- All controls functional, command-line preview updates live
- Version-gated panels show/hide correctly
- Launch button executes GE with correct arguments
- All tests pass including settings round-trip

---

## Phase 4 — Test Completion + Housekeeping ✓ COMPLETE (r1063–r1065)

- Verify locale bug fix on comma-decimal system
- Ensure 100% branch coverage of `THCLCmdLineBuilder.BuildCommandLine`
- Tag last Delphi-compilable SVN revision
- Add `lib/`, `*.exe` to `.svnignore`
- Update `CLAUDE.md` build instructions (Lazarus replaces RAD Studio)

**Acceptance:** All tests pass. `CLAUDE.md` is accurate.

---

## Phase 5 — Future Work (out of scope)

- Restore SE and Other tabs
- Linux: non-Windows `uCICVersionInformation` (currently returns ''), test TProcess path in `uHCLProcessUtils`
- LCL drag-drop for file/directory edits
- ~~High-DPI testing at 150%/200%~~ — Done in Phase 4 (`Scaled=True`, group box anchoring, `edtCmdLineGE` anchor fix)
- SE command-line builder (second `IHCLCmdLineBuilder` implementation)
- Complete `test_Settings.pas` (currently a stub — settings round-trip tests)
- Add version resource (`.rc` file) so `CICGetAppVersionStr` works on the Launcher exe itself

---

## Risk Register

| Risk | Mitigation |
|------|-----------|
| `uCICVersionInformation` change breaks sibling Delphi builds | `{$IFDEF FPC}{$MODE DELPHI}` + plain `SysUtils` are Delphi-transparent. Test a sibling build. |
| Form layout is significant manual effort | Accepted cost. Use `.dfm` as pixel reference. |
| Locale bug fix changes behavior for `.` decimal users | `TryStrToFloat` with `.` separator accepts everything that was previously valid. Only `,` values (which silently misbehaved) are now correctly rejected. |
| `GetFileVersionInfo` not in FPC's Windows unit | Available since FPC 3.2. Confirm version. |

---

## Dependency Order

```
Phase 1 (project files + uHCLProcessUtils + uCICVersionInformation)  ✓ r1061
    ↓
Phase 2 (uHCLCmdLineBuilder + tests)                                 ✓ r1061
    ↓
Phase 3 (uMain1.pas rewrite + uMain1.lfm)                            ✓ r1061
    ↓
Phase 4 (test coverage + high-DPI + housekeeping)                     ✓ r1063
    ↓
Delphi files removed                                                  ✓ r1065
    ↓
Merged lazport → trunk                                                ✓ r1066
```

## SVN Revision Log

| Rev | Description |
|-----|-------------|
| r1059 | Added CLAUDE.md and plan to trunk |
| r1060 | Created `lazport` branch from trunk |
| r1061 | Phases 1–3: full Lazarus port (47 tests) |
| r1062 | Updated shared `uCICVersionInformation.pas` for FPC compat |
| r1063 | Phase 4: 80 tests, high-DPI, CLAUDE.md update |
| r1064 | Tagged `^/HCLauncher/tags/last-delphi` |
| r1065 | Removed Delphi files from lazport branch |
| r1066 | Merged lazport → trunk |

## Porting Issues Encountered

- `PWideChar` → `PChar` in `uCICVersionInformation.pas` (FPC Windows unit uses PChar)
- `TOpenDialog.Options.StrictFiltering` not valid in LCL — caused `EReadError` crash at form load
- FPC-compiled exe has no VERSIONINFO resource by default — `CICGetAppVersionStr` raised exception; wrapped in try/except
- 8700-line TMS `.dfm` could not be converted — `.lfm` built from scratch (1156 lines)
- Phases 1+2 ran in parallel (builder has zero LCL dependency) saving wall-clock time
